diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 1b2696c845e3..fc175297e72c 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -291,6 +291,19 @@ extends: publishArgument: $(_publishArgument) officialBuildProperties: $(_officialBuildProperties) runTests: false + ### ARM64 TESTBUILD ### + - ${{ if or(eq(parameters.runTestBuild, true), eq(variables['Build.Reason'], 'PullRequest')) }}: + - template: /eng/pipelines/templates/jobs/sdk-job-matrix.yml@self + parameters: + pool: + name: Azure Pipelines + vmImage: macOS-latest + os: macOS + helixTargetQueue: osx.13.arm64 + macOSJobParameterSets: + - categoryName: TestBuild + buildArchitecture: arm64 + runtimeIdentifier: osx-arm64 ############### SOURCE BUILD ############### - template: /eng/common/templates-official/job/source-build.yml@self diff --git a/.vsts-pr.yml b/.vsts-pr.yml index 838aa699d3f3..26169a0071f0 100644 --- a/.vsts-pr.yml +++ b/.vsts-pr.yml @@ -63,6 +63,18 @@ stages: vmImage: macOS-latest os: macOS helixTargetQueue: osx.13.amd64.open + ### ARM64 ### + - template: /eng/pipelines/templates/jobs/sdk-job-matrix.yml + parameters: + pool: + name: Azure Pipelines + vmImage: macOS-latest + os: macOS + helixTargetQueue: osx.13.arm64.open + macOSJobParameterSets: + - categoryName: TestBuild + buildArchitecture: arm64 + runtimeIdentifier: osx-arm64 ############### SOURCE BUILD ############### - template: /eng/common/templates/job/source-build.yml@self diff --git a/eng/ManualVersions.props b/eng/ManualVersions.props index 2b773d3a8285..e0f285d96bdc 100644 --- a/eng/ManualVersions.props +++ b/eng/ManualVersions.props @@ -9,20 +9,20 @@ Basically: In this file, choose the highest version when resolving merge conflicts. --> - 10.0.17763.45 - 10.0.18362.45 - 10.0.19041.45 - 10.0.20348.45 - 10.0.22000.45 - 10.0.22621.45 - 10.0.26100.45 - 10.0.17763.43 - 10.0.18362.43 - 10.0.19041.43 - 10.0.20348.43 - 10.0.22000.43 - 10.0.22621.43 - 10.0.26100.43 + 10.0.17763.54 + 10.0.18362.54 + 10.0.19041.54 + 10.0.20348.54 + 10.0.22000.54 + 10.0.22621.54 + 10.0.26100.54 + 10.0.17763.52 + 10.0.18362.52 + 10.0.19041.52 + 10.0.20348.52 + 10.0.22000.52 + 10.0.22621.52 + 10.0.26100.52 diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 818cae010e57..6eb5b3de6f7e 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -37,6 +37,9 @@ + + + diff --git a/eng/Versions.props b/eng/Versions.props index 400fa11fe0ca..087dd733528c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -14,7 +14,6 @@ $(VersionMajor).$(VersionMinor).$(VersionSDKMinor)$(VersionFeature) $(VersionMajor).$(VersionMinor) $(MajorMinorVersion).$(VersionSDKMinor) - 8.0.100 false release @@ -193,7 +192,9 @@ Some .NET Framework tasks and the resolver will need to run in a VS/MSBuild that is older than the very latest, based on what we want the SDK to support. So use a version that matches the version - in minimumMSBuildVersion. In these cases, we don't want to use MicrosoftBuildVersion and other + in minimumMSBuildVersion. Note that MSBuild has started versioning before release so the version we use as the Minimum should be .0 + to ensure we load in VS but the version we build against should be the version of MSBuild that ships in the .0 VS release. + In these cases, we don't want to use MicrosoftBuildVersion and other associated properties that are updated by the VMR infrastructure. So, we read this version from the 'minimumMSBuildVersion' file in non-source-only cases into MicrosoftBuildMinimumVersion, then use that in Directory.Packages.props. @@ -201,7 +202,8 @@ At usage sites, either we use MicrosoftBuildMinimumVersion, or MicrosoftBuildVersion in source-only modes. --> 17.13.0-preview-24604-04 17.13.0-preview-24604-04 - $([System.IO.File]::ReadAllText('$(RepoRoot)src\Layout\redist\minimumMSBuildVersion').Trim()) + 17.11.4 + 17.12 diff --git a/eng/common/core-templates/steps/get-delegation-sas.yml b/eng/common/core-templates/steps/get-delegation-sas.yml index d2901470a7f0..9db5617ea7de 100644 --- a/eng/common/core-templates/steps/get-delegation-sas.yml +++ b/eng/common/core-templates/steps/get-delegation-sas.yml @@ -31,7 +31,16 @@ steps: # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + # Temporarily work around a helix issue where SAS tokens with / in them will cause incorrect downloads + # of correlation payloads. https://github.com/dotnet/dnceng/issues/3484 + $sas = "" + do { + $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to generate SAS token." + exit 1 + } + } while($sas.IndexOf('/') -ne -1) if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." diff --git a/sdk.sln b/sdk.sln index b8659201cec7..c040f0225773 100644 --- a/sdk.sln +++ b/sdk.sln @@ -1150,6 +1150,7 @@ Global src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5 src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5 + src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*SharedItemsImports = 5 src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13 src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5 diff --git a/src/BuiltInTools/AspireService/AspireServerService.cs b/src/BuiltInTools/AspireService/AspireServerService.cs index 3b620131f73f..d3450d65ad18 100644 --- a/src/BuiltInTools/AspireService/AspireServerService.cs +++ b/src/BuiltInTools/AspireService/AspireServerService.cs @@ -1,6 +1,7 @@ // 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; using System.Net; using System.Net.WebSockets; using System.Security.Cryptography; @@ -11,9 +12,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.WebTools.AspireServer.Contracts; using Microsoft.WebTools.AspireServer.Helpers; using Microsoft.WebTools.AspireServer.Models; +using Microsoft.WebTools.AspireService.Helpers; using IAsyncDisposable = System.IAsyncDisposable; namespace Microsoft.WebTools.AspireServer; @@ -32,7 +35,7 @@ internal partial class AspireServerService : IAsyncDisposable private readonly IAspireServerEvents _aspireServerEvents; - private readonly Action? _tracer; + private readonly Action? _reporter; private readonly string _currentSecret; private readonly string _displayName; @@ -46,8 +49,11 @@ internal partial class AspireServerService : IAsyncDisposable private readonly SocketConnectionManager _socketConnectionManager = new(); + private volatile bool _isDisposed; + private static readonly char[] s_charSeparator = { ' ' }; - private int _isListening; + + private readonly Task _requestListener; public static readonly JsonSerializerOptions JsonSerializerOptions = new() { @@ -59,10 +65,10 @@ internal partial class AspireServerService : IAsyncDisposable } }; - public AspireServerService(IAspireServerEvents aspireServerEvents, string displayName, Action? tracer) + public AspireServerService(IAspireServerEvents aspireServerEvents, string displayName, Action? reporter) { _aspireServerEvents = aspireServerEvents; - _tracer = tracer; + _reporter = reporter; _displayName = displayName; _port = SocketUtilities.GetNextAvailablePort(); @@ -79,83 +85,97 @@ public AspireServerService(IAspireServerEvents aspireServerEvents, string displa var certBytes = _certificate.Export(X509ContentType.Cert); _certificateEncodedBytes = Convert.ToBase64String(certBytes); - // Start the server - Initialize(); + // Kick of the web server. + _requestListener = StartListening(); } - /// - public ValueTask>> GetServerConnectionEnvironmentAsync(CancellationToken cancelToken) + public async ValueTask DisposeAsync() { - return new ValueTask>>(new List> - { - new KeyValuePair(DebugSessionPortEnvVar,$"localhost:{_port}"), - new KeyValuePair(DebugSessionTokenEnvVar, _currentSecret), - new KeyValuePair(DebugSessionServerCertEnvVar, _certificateEncodedBytes), - }); - } + // Shutdown the service: + _shutdownCancellationTokenSource.Cancel(); - public async ValueTask SessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelToken) - { - var payload = new SessionChangeNotification() - { - NotificationType = NotificationType.SessionTerminated, - SessionId = sessionId, - PID = processId, - ExitCode = exitCode - }; + Log("Waiting for server to shutdown ..."); try { - LogTrace($"Sending SessionEndedAsync for session {sessionId}"); - var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions); - await SendMessageAsync(dcpId, jsonSerialized, cancelToken); + await _requestListener; } - catch (Exception ex) + catch (OperationCanceledException) { - // Send messageAsync can fail if the connection is lost - LogTrace($"Sending session ended failed: {ex}"); + // nop } + + _isDisposed = true; + + _socketConnectionManager.Dispose(); + _certificate.Dispose(); + _shutdownCancellationTokenSource.Dispose(); } - public async ValueTask SessionStartedAsync(string dcpId, string sessionId, int processId, CancellationToken cancelToken) + /// + public List> GetServerConnectionEnvironment() + => + [ + new(DebugSessionPortEnvVar, $"localhost:{_port}"), + new(DebugSessionTokenEnvVar, _currentSecret), + new(DebugSessionServerCertEnvVar, _certificateEncodedBytes), + ]; + + public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelationToken) + => SendNotificationAsync( + new SessionTerminatedNotification() + { + NotificationType = NotificationType.SessionTerminated, + SessionId = sessionId, + Pid = processId, + ExitCode = exitCode + }, + dcpId, + sessionId, + cancelationToken); + + public ValueTask NotifySessionStartedAsync(string dcpId, string sessionId, int processId, CancellationToken cancelationToken) + => SendNotificationAsync( + new ProcessRestartedNotification() + { + NotificationType = NotificationType.ProcessRestarted, + SessionId = sessionId, + PID = processId + }, + dcpId, + sessionId, + cancelationToken); + + public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isStdErr, string data, CancellationToken cancelationToken) + => SendNotificationAsync( + new ServiceLogsNotification() + { + NotificationType = NotificationType.ServiceLogs, + SessionId = sessionId, + IsStdErr = isStdErr, + LogMessage = data + }, + dcpId, + sessionId, + cancelationToken); + + private async ValueTask SendNotificationAsync(TNotification notification, string dcpId, string sessionId, CancellationToken cancelationToken) + where TNotification : SessionNotification { - var payload = new SessionChangeNotification() - { - NotificationType = NotificationType.ProcessRestarted, - SessionId = sessionId, - PID = processId - }; - try { - LogTrace($"Sending SessionStartedAsync for session {sessionId}"); - var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions); - await SendMessageAsync(dcpId, jsonSerialized, cancelToken); + Log($"[#{sessionId}] Sending '{notification.NotificationType}'"); + var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(notification, JsonSerializerOptions); + await SendMessageAsync(dcpId, jsonSerialized, cancelationToken); } - catch (Exception ex) + catch (Exception e) when (LogAndPropagate(e)) { - LogTrace($"Sending session started failed: {ex}"); } - } - - public async ValueTask SendLogMessageAsync(string dcpId, string sessionID, bool isStdErr, string data, CancellationToken cancelToken) - { - var payload = new SessionLogsNotification() - { - NotificationType = NotificationType.ServiceLogs, - SessionId = sessionID, - IsStdErr = isStdErr, - LogMessage = data - }; - try + bool LogAndPropagate(Exception e) { - var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions); - await SendMessageAsync(dcpId, jsonSerialized, cancelToken); - } - catch (Exception ex) - { - LogTrace($"Sending service logs failed {ex}"); + Log($"[#{sessionId}] Sending '{notification.NotificationType}' failed: {e.Message}"); + return false; } } @@ -163,7 +183,7 @@ public async ValueTask SendLogMessageAsync(string dcpId, string sessionID, bool /// Waits for a connection so that it can get the WebSocket that will be used to send messages tio the client. It accepts messages via Restful http /// calls. /// - private void StartListening() + private Task StartListening() { var builder = WebApplication.CreateSlimBuilder(); @@ -175,6 +195,12 @@ private void StartListening() }); }); + if (_reporter != null) + { + builder.Logging.ClearProviders(); + builder.Logging.AddProvider(new LoggerProvider(_reporter)); + } + var app = builder.Build(); app.MapGet("/", () => _displayName); @@ -185,7 +211,7 @@ private void StartListening() runSessionApi.MapPut("/", RunSessionPutAsync); runSessionApi.MapDelete("/{sessionId}", RunSessionDeleteAsync); - runSessionApi.Map(SessionNotificationBase.Url, RunSessionNotifyAsync); + runSessionApi.Map(SessionNotification.Url, RunSessionNotifyAsync); app.UseWebSockets(new WebSocketOptions { @@ -193,7 +219,7 @@ private void StartListening() }); // Run the application async. It will shutdown when the cancel token is signaled - _ = app.RunAsync(_shutdownCancellationTokenSource.Token); + return app.RunAsync(_shutdownCancellationTokenSource.Token); } private async Task RunSessionPutAsync(HttpContext context) @@ -201,12 +227,12 @@ private async Task RunSessionPutAsync(HttpContext context) // Check the authentication header if (!IsValidAuthentication(context)) { - LogTrace("Authorization failure"); + Log("Authorization failure"); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else { - await ProcessStartSessionRequestAsync(context); + await HandleStartSessionRequestAsync(context); } } @@ -215,12 +241,12 @@ private async Task RunSessionDeleteAsync(HttpContext context, string sessionId) // Check the authentication header if (!IsValidAuthentication(context)) { - LogTrace("Authorization failure"); + Log("Authorization failure"); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else { - context.Response.StatusCode = await HandleStopSessionRequestAsync(context.GetDcpId(), sessionId); + await HandleStopSessionRequestAsync(context, sessionId); } } @@ -229,7 +255,7 @@ private async Task GetInfoAsync(HttpContext context) // Check the authentication header if (!IsValidAuthentication(context)) { - LogTrace("Authorization failure"); + Log("Authorization failure"); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else @@ -244,7 +270,7 @@ private async Task RunSessionNotifyAsync(HttpContext context) // Check the authentication header if (!IsValidAuthentication(context)) { - LogTrace("Authorization failure"); + Log("Authorization failure"); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } @@ -264,35 +290,9 @@ private async Task RunSessionNotifyAsync(HttpContext context) await socketTcs.Task; } - private void LogTrace(string traceMsg) - { - _tracer?.Invoke($"AspireServer - {traceMsg}"); - } - - /// - /// starts the web server running - /// - private void Initialize() - { - if (Interlocked.CompareExchange(ref _isListening, 1, 0) == 1) - { - return; - } - - // Kick of the web server. - StartListening(); - } - - public ValueTask DisposeAsync() + private void Log(string message) { - _socketConnectionManager.Dispose(); - - _certificate.Dispose(); - - // Shutdown the app - _shutdownCancellationTokenSource.Cancel(); - _shutdownCancellationTokenSource.Dispose(); - return ValueTask.CompletedTask; + _reporter?.Invoke(message); } private bool IsValidAuthentication(HttpContext context) @@ -311,29 +311,39 @@ private bool IsValidAuthentication(HttpContext context) return false; } - private async Task ProcessStartSessionRequestAsync(HttpContext context) + private async Task HandleStartSessionRequestAsync(HttpContext context) { - // Get the project launch request data - var projectLaunchRequest = await context.GetProjectLaunchInformationAsync(_shutdownCancellationTokenSource.Token); - if (projectLaunchRequest is not null) + string? projectPath = null; + + try { - try + if (_isDisposed) { - var sessionId = await LaunchProjectAsync(context.GetDcpId(), projectLaunchRequest); - context.Response.StatusCode = (int)HttpStatusCode.Created; - context.Response.Headers.Location = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}/{sessionId}"; + throw new ObjectDisposedException(nameof(AspireServerService), "Received 'PUT /run_session' request after the service has been disposed."); } - catch (Exception ex) + + // Get the project launch request data + var projectLaunchRequest = await context.GetProjectLaunchInformationAsync(_shutdownCancellationTokenSource.Token); + if (projectLaunchRequest == null) { - LogTrace($"Exception thrown starting project {projectLaunchRequest.ProjectPath}: {ex}"); - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - await WriteResponseTextAsync(context.Response, ex, context.GetApiVersion() is not null); + // Unknown or unsupported version + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return; } + + projectPath = projectLaunchRequest.ProjectPath; + + var sessionId = await _aspireServerEvents.StartProjectAsync(context.GetDcpId(), projectLaunchRequest, _shutdownCancellationTokenSource.Token); + + context.Response.StatusCode = (int)HttpStatusCode.Created; + context.Response.Headers.Location = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}/{sessionId}"; } - else + catch (Exception e) { - // Unknown or unsupported version - context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + Log($"Failed to start project{(projectPath == null ? "" : $" '{projectPath}'")}: {e}"); + + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await WriteResponseTextAsync(context.Response, e, context.GetApiVersion() is not null); } } @@ -361,48 +371,58 @@ private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, b } } - private async Task SendMessageAsync(string dcpId,byte[] messageBytes, CancellationToken cancellationToken) + private async Task SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken) { // Find the connection for the passed in dcpId WebSocketConnection? connection = _socketConnectionManager.GetSocketConnection(dcpId); if (connection is null) { // Most likely the connection has already gone away - LogTrace($"Send message failure: Connection with the following dcpId was not found {dcpId}"); + Log($"Send message failure: Connection with the following dcpId was not found {dcpId}"); return; } + var success = false; try { - using var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCancellationTokenSource.Token, - connection.HttpRequestAborted); + using var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, _shutdownCancellationTokenSource.Token, connection.HttpRequestAborted); + await _webSocketAccess.WaitAsync(cancelTokenSource.Token); await connection.Socket.SendAsync(new ArraySegment(messageBytes), WebSocketMessageType.Text, endOfMessage: true, cancelTokenSource.Token); - } - catch (Exception ex) - { - // If the connection throws it almost certainly means the client has gone away, so clean up that connection - _socketConnectionManager.RemoveSocketConnection(connection); - LogTrace($"Send message failure: {ex.GetMessageFromException()}"); - throw; + + success = true; } finally { + if (!success) + { + // If the connection throws it almost certainly means the client has gone away, so clean up that connection + _socketConnectionManager.RemoveSocketConnection(connection); + } + _webSocketAccess.Release(); } } - private async Task HandleStopSessionRequestAsync(string dcpId, string sessionId) + private async ValueTask HandleStopSessionRequestAsync(HttpContext context, string sessionId) { - bool sessionExists = await _aspireServerEvents.StopSessionAsync(dcpId, sessionId, _shutdownCancellationTokenSource.Token); + try + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AspireServerService), "Received 'DELETE /run_session' request after the service has been disposed."); + } - return (int)(sessionExists ? HttpStatusCode.OK : HttpStatusCode.NoContent); - } + var sessionExists = await _aspireServerEvents.StopSessionAsync(context.GetDcpId(), sessionId, _shutdownCancellationTokenSource.Token); + context.Response.StatusCode = (int)(sessionExists ? HttpStatusCode.OK : HttpStatusCode.NoContent); + } + catch (Exception e) + { + Log($"[#{sessionId}] Failed to stop: {e}"); - /// - /// Called to launch the project after first creating a LaunchProfile from the sessionRequest object. Returns the sessionId - /// for the launched process. If it throws an exception most likely the project couldn't be launched - /// - private Task LaunchProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo) - => _aspireServerEvents.StartProjectAsync(dcpId, projectLaunchInfo, _shutdownCancellationTokenSource.Token).AsTask(); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await WriteResponseTextAsync(context.Response, e, context.GetApiVersion() is not null); + } + } } diff --git a/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs b/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs index d4d30c20663f..1a4b5b6ddee2 100644 --- a/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs +++ b/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs @@ -1,30 +1,24 @@ // 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.Generic; -using System.Threading; -using System.Threading.Tasks; - namespace Microsoft.WebTools.AspireServer.Contracts; -/// -/// Interface implemented on the VS side and pass -/// internal interface IAspireServerEvents { /// - /// Called when a request to stop a session is received. Returns false if the session does not exist. Note that the dcpId identifies - /// which DCP/AppHost is making the request. + /// Called when a request to stop a session is received. /// - ValueTask StopSessionAsync(string dcpId, string sessionId, CancellationToken cancelToken); + /// The id of the session to terminate. The session might have been stopped already. + /// DCP/AppHost making the request. May be empty for older DCP versions. + /// Returns false if the session is not active. + ValueTask StopSessionAsync(string dcpId, string sessionId, CancellationToken cancellationToken); /// - /// Called when a request to start a project is received. Returns the sessionId of the started project. Note that the dcpId identifies - /// which DCP/AppHost is making the request. The format of this string is ;. The first token can - /// be used to identify the AppHost project in the solution. The 2nd is just a unique string so that running the same project multiple times - /// generates a unique dcpId. Note that for older DCP's the dcpId will be the empty string + /// Called when a request to start a project is received. Returns the session id of the started project. /// - ValueTask StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancelToken); + /// DCP/AppHost making the request. May be empty for older DCP versions. + /// New unique session id. + ValueTask StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken); } internal class ProjectLaunchRequest diff --git a/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs b/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs new file mode 100644 index 000000000000..52953e211022 --- /dev/null +++ b/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs @@ -0,0 +1,36 @@ +// 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.WebTools.AspireService.Helpers; + +internal sealed class LoggerProvider(Action reporter) : ILoggerProvider +{ + private sealed class Logger(Action reporter) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var message = formatter(state, exception); + if (!string.IsNullOrEmpty(message)) + { + reporter(message); + } + } + } + + public void Dispose() + { + } + + public Action Reporter + => reporter; + + public ILogger CreateLogger(string categoryName) + => new Logger(reporter); +} diff --git a/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.projitems b/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.projitems index 7092eff2441a..2fa482c32f7e 100644 --- a/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.projitems +++ b/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.projitems @@ -14,6 +14,7 @@ + diff --git a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs index 49ad7e790858..d20e38f3360f 100644 --- a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs +++ b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs @@ -13,32 +13,80 @@ internal static class NotificationType public const string ServiceLogs = "serviceLogs"; } -internal class SessionNotificationBase +/// +/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#common-notification-properties. +/// +internal class SessionNotification { public const string Url = "/notify"; + /// + /// One of . + /// [Required] [JsonPropertyName("notification_type")] - public string NotificationType { get; set; } = string.Empty; + public required string NotificationType { get; init; } + /// + /// The id of the run session that the notification is related to. + /// + [Required] [JsonPropertyName("session_id")] - public string SessionId { get; set; } = string.Empty; + public required string SessionId { get; init; } } -internal class SessionChangeNotification : SessionNotificationBase +/// +/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#session-terminated-notification. +/// is . +/// +internal sealed class SessionTerminatedNotification : SessionNotification { + /// + /// The process id of the service process associated with the run session. + /// + [Required] [JsonPropertyName("pid")] - public int PID { get; set; } + public required int Pid { get; init; } + /// + /// The exit code of the process associated with the run session. + /// + [Required] [JsonPropertyName("exit_code")] - public int? ExitCode { get; set; } + public required int? ExitCode { get; init; } +} + +/// +/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#process-restarted-notification. +/// is . +/// +internal sealed class ProcessRestartedNotification : SessionNotification +{ + /// + /// The process id of the service process associated with the run session. + /// + [Required] + [JsonPropertyName("pid")] + public required int PID { get; init; } } -internal class SessionLogsNotification : SessionNotificationBase +/// +/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#log-notification +/// is . +/// +internal sealed class ServiceLogsNotification : SessionNotification { + /// + /// True if the output comes from standard error stream, otherwise false (implying standard output stream). + /// + [Required] [JsonPropertyName("is_std_err")] - public bool IsStdErr { get; set; } + public required bool IsStdErr { get; init; } + /// + /// The text written by the service program. + /// + [Required] [JsonPropertyName("log_message")] - public string LogMessage { get; set; } = string.Empty; + public required string LogMessage { get; init; } } diff --git a/src/BuiltInTools/dotnet-watch.slnf b/src/BuiltInTools/dotnet-watch.slnf index 94e287feeca5..5baaf69115c2 100644 --- a/src/BuiltInTools/dotnet-watch.slnf +++ b/src/BuiltInTools/dotnet-watch.slnf @@ -11,7 +11,8 @@ "test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", - "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj" + "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj", + "test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj" ] } } \ No newline at end of file diff --git a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs new file mode 100644 index 000000000000..753d6dd66cfb --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs @@ -0,0 +1,262 @@ +// 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; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Channels; +using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; +using Microsoft.DotNet.Watcher.Tools; +using Microsoft.Extensions.Tools.Internal; +using Microsoft.WebTools.AspireServer; +using Microsoft.WebTools.AspireServer.Contracts; + +namespace Microsoft.DotNet.Watcher; + +internal class AspireServiceFactory : IRuntimeProcessLauncherFactory +{ + private sealed class SessionManager : IAspireServerEvents, IRuntimeProcessLauncher + { + private readonly struct Session(string dcpId, string sessionId, RunningProject runningProject, Task outputReader) + { + public string DcpId { get; } = dcpId; + public string Id { get; } = sessionId; + public RunningProject RunningProject { get; } = runningProject; + public Task OutputReader { get; } = outputReader; + } + + private static readonly UnboundedChannelOptions s_outputChannelOptions = new() + { + SingleReader = true, + SingleWriter = true + }; + + private readonly ProjectLauncher _projectLauncher; + private readonly AspireServerService _service; + private readonly IReadOnlyList<(string name, string value)> _buildProperties; + + /// + /// Lock to access: + /// + /// + /// + private readonly object _guard = new(); + + private readonly Dictionary _sessions = []; + private int _sessionIdDispenser; + private volatile bool _isDisposed; + + public SessionManager(ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties) + { + _projectLauncher = projectLauncher; + _buildProperties = buildProperties; + + _service = new AspireServerService( + this, + displayName: ".NET Watch Aspire Server", + m => projectLauncher.Reporter.Verbose(m, MessageEmoji)); + } + + public async ValueTask DisposeAsync() + { +#if DEBUG + lock (_guard) + { + Debug.Assert(_sessions.Count == 0); + } +#endif + _isDisposed = true; + + await _service.DisposeAsync(); + } + + public async ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + ImmutableArray sessions; + lock (_guard) + { + // caller guarantees the session is active + sessions = [.. _sessions.Values]; + _sessions.Clear(); + } + + foreach (var session in sessions) + { + await TerminateSessionAsync(session, cancellationToken); + } + } + + public IEnumerable<(string name, string value)> GetEnvironmentVariables() + => _service.GetServerConnectionEnvironment().Select(kvp => (kvp.Key, kvp.Value)); + + private IReporter Reporter + => _projectLauncher.Reporter; + + /// + /// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#create-session-request. + /// + async ValueTask IAspireServerEvents.StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + var projectOptions = GetProjectOptions(projectLaunchInfo); + var sessionId = Interlocked.Increment(ref _sessionIdDispenser).ToString(CultureInfo.InvariantCulture); + await StartProjectAsync(dcpId, sessionId, projectOptions, build: false, isRestart: false, cancellationToken); + return sessionId; + } + + public async ValueTask StartProjectAsync(string dcpId, string sessionId, ProjectOptions projectOptions, bool build, bool isRestart, CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Reporter.Verbose($"Starting project: {projectOptions.ProjectPath}", MessageEmoji); + + var processTerminationSource = new CancellationTokenSource(); + var outputChannel = Channel.CreateUnbounded(s_outputChannelOptions); + + var runningProject = await _projectLauncher.TryLaunchProcessAsync( + projectOptions, + processTerminationSource, + onOutput: line => + { + var writeResult = outputChannel.Writer.TryWrite(line); + Debug.Assert(writeResult); + }, + restartOperation: (build, cancellationToken) => + StartProjectAsync(dcpId, sessionId, projectOptions, build, isRestart: true, cancellationToken), + build: build, + cancellationToken); + + if (runningProject == null) + { + // detailed error already reported: + throw new ApplicationException($"Failed to launch project '{projectOptions.ProjectPath}'."); + } + + await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken); + + // cancel reading output when the process terminates: + var outputReader = StartChannelReader(processTerminationSource.Token); + + lock (_guard) + { + // When process is restarted we reuse the session id. + // The session already exists, it needs to be updated with new info. + Debug.Assert(_sessions.ContainsKey(sessionId) == isRestart); + + _sessions[sessionId] = new Session(dcpId, sessionId, runningProject, outputReader); + } + + Reporter.Verbose($"Session started: #{sessionId}", MessageEmoji); + return runningProject; + + async Task StartChannelReader(CancellationToken cancellationToken) + { + try + { + await foreach (var line in outputChannel.Reader.ReadAllAsync(cancellationToken)) + { + await _service.NotifyLogMessageAsync(dcpId, sessionId, isStdErr: line.IsError, data: line.Content, cancellationToken); + } + } + catch (OperationCanceledException) + { + // nop + } + catch (Exception e) + { + Reporter.Error($"Unexpected error reading output of session '{sessionId}': {e}"); + } + } + } + + /// + /// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#stop-session-request. + /// + async ValueTask IAspireServerEvents.StopSessionAsync(string dcpId, string sessionId, CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Session session; + lock (_guard) + { + if (!_sessions.TryGetValue(sessionId, out session)) + { + return false; + } + + _sessions.Remove(sessionId); + } + + await TerminateSessionAsync(session, cancellationToken); + return true; + } + + private async ValueTask TerminateSessionAsync(Session session, CancellationToken cancellationToken) + { + Reporter.Verbose($"Stop session #{session.Id}", MessageEmoji); + + var exitCode = await _projectLauncher.TerminateProcessAsync(session.RunningProject, cancellationToken); + + // Wait until the started notification has been sent so that we don't send out of order notifications: + await _service.NotifySessionEndedAsync(session.DcpId, session.Id, session.RunningProject.ProcessId, exitCode, cancellationToken); + + // process termination should cancel output reader task: + await session.OutputReader; + } + + private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo) + { + var arguments = new List + { + "--project", + projectLaunchInfo.ProjectPath, + // TODO: https://github.com/dotnet/sdk/issues/43946 + // Need to suppress launch profile for now, otherwise it would override the port set via env variable. + "--no-launch-profile", + }; + + //if (projectLaunchInfo.DisableLaunchProfile) + //{ + // arguments.Add("--no-launch-profile"); + //} + //else if (!string.IsNullOrEmpty(projectLaunchInfo.LaunchProfile)) + //{ + // arguments.Add("--launch-profile"); + // arguments.Add(projectLaunchInfo.LaunchProfile); + //} + + if (projectLaunchInfo.Arguments != null) + { + arguments.AddRange(projectLaunchInfo.Arguments); + } + + return new() + { + IsRootProject = false, + ProjectPath = projectLaunchInfo.ProjectPath, + WorkingDirectory = _projectLauncher.EnvironmentOptions.WorkingDirectory, // TODO: Should DCP protocol specify? + BuildProperties = _buildProperties, // TODO: Should DCP protocol specify? + Command = "run", + CommandArguments = arguments, + LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(kvp => (kvp.Key, kvp.Value)).ToArray() ?? [], + LaunchProfileName = projectLaunchInfo.LaunchProfile, + NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile, + TargetFramework = null, // TODO: Should DCP protocol specify? + }; + } + } + + public const string MessageEmoji = "⭐"; + + public static readonly AspireServiceFactory Instance = new(); + public const string AppHostProjectCapability = "Aspire"; + + public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties) + => projectNode.GetCapabilities().Contains(AppHostProjectCapability) + ? new SessionManager(projectLauncher, buildProperties) + : null; +} diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs index e5e90a0bf361..21d2f2cfce13 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools @@ -65,8 +66,12 @@ await Task.WhenAll(serversToDispose.Select(async server => } } - // Attach trigger to the process that launches browser on URL found in the process output: - processSpec.OnOutput += GetBrowserLaunchTrigger(projectNode, projectOptions, server, cancellationToken); + // Attach trigger to the process that launches browser on URL found in the process output. + // Only do so for root projects, not for child processes. + if (projectOptions.IsRootProject) + { + processSpec.OnOutput += GetBrowserLaunchTrigger(projectNode, projectOptions, server, cancellationToken); + } if (server == null) { @@ -100,7 +105,7 @@ public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true) /// /// Get process output handler that will be subscribed to the process output event every time the process is launched. /// - public DataReceivedEventHandler? GetBrowserLaunchTrigger(ProjectGraphNode projectNode, ProjectOptions projectOptions, BrowserRefreshServer? server, CancellationToken cancellationToken) + public Action? GetBrowserLaunchTrigger(ProjectGraphNode projectNode, ProjectOptions projectOptions, BrowserRefreshServer? server, CancellationToken cancellationToken) { if (!CanLaunchBrowser(context, projectNode, projectOptions, out var launchProfile)) { @@ -112,20 +117,27 @@ public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true) return null; } + bool matchFound = false; + return handler; - void handler(object sender, DataReceivedEventArgs eventArgs) + void handler(OutputLine line) { // We've redirected the output, but want to ensure that it continues to appear in the user's console. - Console.WriteLine(eventArgs.Data); + (line.IsError ? Console.Error : Console.Out).WriteLine(line.Content); + + if (matchFound) + { + return; + } - var match = s_nowListeningRegex.Match(eventArgs.Data ?? ""); + var match = s_nowListeningRegex.Match(line.Content); if (!match.Success) { return; } - ((Process)sender).OutputDataReceived -= handler; + matchFound = true; var projectAddedToAttemptedSet = ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, projectNode) => set.Add(projectNode), projectNode); if (projectAddedToAttemptedSet) diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs index 52fbc003ed1f..c3547bd5b146 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs @@ -209,6 +209,8 @@ public async ValueTask SendJsonWithSecret(Func valueFac { try { + bool messageSent = false; + for (var i = 0; i < _clientSockets.Count; i++) { var (clientSocket, secret) = _clientSockets[i]; @@ -221,7 +223,10 @@ public async ValueTask SendJsonWithSecret(Func valueFac var messageBytes = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions); await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken); + messageSent = true; } + + _reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open."); } catch (TaskCanceledException) { @@ -237,6 +242,8 @@ public async ValueTask SendMessage(ReadOnlyMemory messageBytes, Cancellati { try { + bool messageSent = false; + for (var i = 0; i < _clientSockets.Count; i++) { var (clientSocket, _) = _clientSockets[i]; @@ -244,8 +251,12 @@ public async ValueTask SendMessage(ReadOnlyMemory messageBytes, Cancellati { continue; } + await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken); + messageSent = true; } + + _reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open."); } catch (TaskCanceledException) { diff --git a/src/BuiltInTools/dotnet-watch/DotNetWatch.targets b/src/BuiltInTools/dotnet-watch/DotNetWatch.targets index 799c29ad1f55..c0ef2c555789 100644 --- a/src/BuiltInTools/dotnet-watch/DotNetWatch.targets +++ b/src/BuiltInTools/dotnet-watch/DotNetWatch.targets @@ -72,7 +72,7 @@ Returns: @(Watch) Condition="'$(UsingMicrosoftNETSdkRazor)'=='true' AND '$(DotNetWatchContentFiles)'!='false' AND '%(Content.Watch)' != 'false' AND $([System.String]::Copy('%(Identity)').Replace('\','/').StartsWith('wwwroot/'))" StaticWebAssetPath="$(_DotNetWatchStaticWebAssetBasePath)$([System.String]::Copy('%(Identity)').Replace('\','/').Substring(8))" /> - <_WatchProjects Include="%(ProjectReference.Identity)" Condition="'%(ProjectReference.Watch)' != 'false'" /> + <_WatchProjects Include="%(ProjectReference.Identity)" Condition="'%(ProjectReference.Watch)' != 'false' and Exists('%(Identity)')" /> ((TaskCompletionSource)state!).TrySetResult(), + shutdownCancellationToken.Register(state => ((TaskCompletionSource)state!).TrySetResult(), cancelledTaskSource); if (Context.EnvironmentOptions.SuppressMSBuildIncrementalism) @@ -31,29 +29,29 @@ public override async Task WatchAsync(CancellationToken cancellationToken) var buildEvaluator = new BuildEvaluator(Context, RootFileSetFactory); await using var browserConnector = new BrowserConnector(Context); - StaticFileHandler? staticFileHandler; - ProjectGraphNode? projectRootNode; - if (Context.ProjectGraph != null) - { - projectRootNode = Context.ProjectGraph.GraphRoots.Single(); - var projectMap = new ProjectNodeMap(Context.ProjectGraph, Context.Reporter); - staticFileHandler = new StaticFileHandler(Context.Reporter, projectMap, browserConnector); - } - else - { - Context.Reporter.Verbose("Unable to determine if this project is a webapp."); - projectRootNode = null; - staticFileHandler = null; - } - for (var iteration = 0;;iteration++) { - if (await buildEvaluator.EvaluateAsync(changedFile, cancellationToken) is not { } evaluationResult) + if (await buildEvaluator.EvaluateAsync(changedFile, shutdownCancellationToken) is not { } evaluationResult) { Context.Reporter.Error("Failed to find a list of files to watch"); return; } + StaticFileHandler? staticFileHandler; + ProjectGraphNode? projectRootNode; + if (evaluationResult.ProjectGraph != null) + { + projectRootNode = evaluationResult.ProjectGraph.GraphRoots.Single(); + var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter); + staticFileHandler = new StaticFileHandler(Context.Reporter, projectMap, browserConnector); + } + else + { + Context.Reporter.Verbose("Unable to determine if this project is a webapp."); + projectRootNode = null; + staticFileHandler = null; + } + var processSpec = new ProcessSpec { Executable = Context.EnvironmentOptions.MuxerPath, @@ -67,7 +65,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) }; var browserRefreshServer = (projectRootNode != null) - ? await browserConnector.LaunchOrRefreshBrowserAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, cancellationToken) + ? await browserConnector.LaunchOrRefreshBrowserAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, shutdownCancellationToken) : null; environmentBuilder.ConfigureProcess(processSpec); @@ -75,16 +73,16 @@ public override async Task WatchAsync(CancellationToken cancellationToken) // Reset for next run buildEvaluator.RequiresRevaluation = false; - if (cancellationToken.IsCancellationRequested) + if (shutdownCancellationToken.IsCancellationRequested) { return; } using var currentRunCancellationSource = new CancellationTokenSource(); - using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, currentRunCancellationSource.Token); + using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, currentRunCancellationSource.Token); using var fileSetWatcher = new FileWatcher(evaluationResult.Files, Context.Reporter); - var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, processExitedSource: null, combinedCancellationSource.Token); + var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token); Task fileSetTask; Task finishedTask; @@ -112,7 +110,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) await Task.WhenAll(processTask, fileSetTask); - if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested) + if (finishedTask == cancelledTaskSource.Task || shutdownCancellationToken.IsCancellationRequested) { return; } @@ -124,7 +122,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken) // Now wait for a file to change before restarting process changedFile = await fileSetWatcher.GetChangedFileAsync( () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting), - cancellationToken); + shutdownCancellationToken); } else { diff --git a/src/BuiltInTools/dotnet-watch/EvaluationResult.cs b/src/BuiltInTools/dotnet-watch/EvaluationResult.cs index b796976b84be..48ca7f6902af 100644 --- a/src/BuiltInTools/dotnet-watch/EvaluationResult.cs +++ b/src/BuiltInTools/dotnet-watch/EvaluationResult.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Build.Graph; + namespace Microsoft.DotNet.Watcher; -internal sealed class EvaluationResult(IReadOnlyDictionary files) +internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph? projectGraph) { public readonly IReadOnlyDictionary Files = files; + public readonly ProjectGraph? ProjectGraph = projectGraph; } diff --git a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs index 0025fbbbcbf5..3221f60278f2 100644 --- a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs +++ b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs @@ -78,7 +78,7 @@ private async ValueTask CreateEvaluationResult(CancellationTok { cancellationToken.ThrowIfCancellationRequested(); - var result = await rootProjectFileSetFactory.TryCreateAsync(cancellationToken); + var result = await rootProjectFileSetFactory.TryCreateAsync(requireProjectGraph: true, cancellationToken); if (result != null) { return result; diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs index cdfc4cf3f9c2..73b3329f8fdb 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs @@ -9,13 +9,12 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; -using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools { - internal sealed class CompilationHandler : IAsyncDisposable + internal sealed class CompilationHandler : IDisposable { public readonly IncrementalMSBuildWorkspace Workspace; @@ -55,38 +54,24 @@ public CompilationHandler(IReporter reporter) _hotReloadService = new WatchHotReloadService(Workspace.CurrentSolution.Services, GetAggregateCapabilitiesAsync); } - public async ValueTask DisposeAsync() + public void Dispose() { _isDisposed = true; - Workspace?.Dispose(); - - IEnumerable projects; - lock (_runningProjectsAndUpdatesGuard) - { - projects = _runningProjects.SelectMany(entry => entry.Value).Where(p => !p.Options.IsRootProject); - _runningProjects = _runningProjects.Clear(); - } - - await TerminateAndDisposeRunningProjects(projects); } - private static async ValueTask TerminateAndDisposeRunningProjects(IEnumerable projects) + public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken) { - // cancel first, this will cause the process tasks to complete: - foreach (var project in projects) - { - project.ProcessTerminationSource.Cancel(); - } + _reporter.Verbose("Disposing remaining child processes."); - // wait for all tasks to complete: - await Task.WhenAll(projects.Select(p => p.RunningProcess)).WaitAsync(CancellationToken.None); + var projectsToDispose = await TerminateNonRootProcessesAsync(projectPaths: null, cancellationToken); - // dispose only after all tasks have completed to prevent the tasks from accessing disposed resources: - foreach (var project in projects) + foreach (var project in projectsToDispose) { project.Dispose(); } + + Dispose(); } public ValueTask RestartSessionAsync(IReadOnlySet projectsToBeRebuilt, CancellationToken cancellationToken) @@ -124,12 +109,13 @@ private DeltaApplier CreateDeltaApplier(ProjectGraphNode projectNode, BrowserRef _ => new DefaultDeltaApplier(processReporter), }; - public async Task TrackRunningProjectAsync( + public async Task TrackRunningProjectAsync( ProjectGraphNode projectNode, ProjectOptions projectOptions, string namedPipeName, BrowserRefreshServer? browserRefreshServer, ProcessSpec processSpec, + RestartOperation restartOperation, IReporter processReporter, CancellationTokenSource processTerminationSource, CancellationToken cancellationToken) @@ -146,7 +132,20 @@ public async Task TrackRunningProjectAsync( // It is important to first create the named pipe connection (delta applier is the server) // and then start the process (named pipe client). Otherwise, the connection would fail. deltaApplier.CreateConnection(namedPipeName, processCommunicationCancellationSource.Token); - var runningProcess = ProcessRunner.RunAsync(processSpec, processReporter, isUserApplication: true, processExitedSource, processTerminationSource.Token); + + processSpec.OnExit += (_, _) => + { + processExitedSource.Cancel(); + return ValueTask.CompletedTask; + }; + + var launchResult = new ProcessLaunchResult(); + var runningProcess = ProcessRunner.RunAsync(processSpec, processReporter, isUserApplication: true, launchResult, processTerminationSource.Token); + if (launchResult.ProcessId == null) + { + // error already reported + return null; + } var capabilityProvider = deltaApplier.GetApplyUpdateCapabilitiesAsync(processCommunicationCancellationSource.Token); var runningProject = new RunningProject( @@ -156,8 +155,10 @@ public async Task TrackRunningProjectAsync( processReporter, browserRefreshServer, runningProcess, + launchResult.ProcessId.Value, processExitedSource: processExitedSource, processTerminationSource: processTerminationSource, + restartOperation: restartOperation, disposables: [processCommunicationCancellationSource], capabilityProvider); @@ -281,6 +282,8 @@ private static void PrepareCompilations(Solution solution, string projectPath, C var runningProjects = _runningProjects; var updates = await _hotReloadService.GetUpdatesAsync(currentSolution, isRunningProject: p => runningProjects.ContainsKey(p.FilePath!), cancellationToken); + var anyProcessNeedsRestart = updates.ProjectsToRestart.Count > 0; + await DisplayResultsAsync(updates, cancellationToken); if (updates.Status is ModuleUpdateStatus.None or ModuleUpdateStatus.Blocked) @@ -292,7 +295,10 @@ private static void PrepareCompilations(Solution solution, string projectPath, C if (updates.Status == ModuleUpdateStatus.RestartRequired) { - Debug.Assert(updates.ProjectsToRestart.Count > 0); + if (!anyProcessNeedsRestart) + { + return (ImmutableHashSet.Empty, []); + } await restartPrompt.Invoke(updates.ProjectsToRestart, cancellationToken); @@ -345,6 +351,8 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates updates, CancellationToken cancellationToken) { + var anyProcessNeedsRestart = updates.ProjectsToRestart.Count > 0; + switch (updates.Status) { case ModuleUpdateStatus.None: @@ -355,7 +363,15 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates update break; case ModuleUpdateStatus.RestartRequired: - _reporter.Output("Unable to apply hot reload, restart is needed to apply the changes."); + if (anyProcessNeedsRestart) + { + _reporter.Output("Unable to apply hot reload, restart is needed to apply the changes."); + } + else + { + _reporter.Verbose("Rude edits detected but do not affect any running process"); + } + break; case ModuleUpdateStatus.Blocked: @@ -418,6 +434,12 @@ void Display(MessageSeverity severity) continue; } + // Do not report rude edits as errors/warnings if no running process is affected. + if (!anyProcessNeedsRestart && diagnostic.Id is ['E', 'N', 'C', >= '0' and <= '9', ..]) + { + descriptor = descriptor with { Severity = MessageSeverity.Verbose }; + } + var display = CSharpDiagnosticFormatter.Instance.Format(diagnostic); _reporter.Report(descriptor, display); @@ -436,36 +458,94 @@ await ForEachProjectAsync( } /// - /// Terminates all processes launched for projects with . + /// Terminates all processes launched for projects with , + /// or all running non-root project processes if is null. + /// /// Removes corresponding entries from . /// - /// May terminate the root project process as well. + /// Does not terminate the root project. /// - internal async ValueTask> TerminateNonRootProcessesAsync(IEnumerable projectPaths, CancellationToken cancellationToken) + internal async ValueTask> TerminateNonRootProcessesAsync( + IEnumerable? projectPaths, CancellationToken cancellationToken) { - IEnumerable projectsToRestart; - lock (_runningProjectsAndUpdatesGuard) - { - // capture snapshot of running processes that can be enumerated outside of the lock: - var runningProjects = _runningProjects; - projectsToRestart = projectPaths.SelectMany(path => runningProjects[path]); + ImmutableArray projectsToRestart = []; - _runningProjects = runningProjects.RemoveRange(projectPaths); + UpdateRunningProjects(runningProjectsByPath => + { + if (projectPaths == null) + { + projectsToRestart = _runningProjects.SelectMany(entry => entry.Value).Where(p => !p.Options.IsRootProject).ToImmutableArray(); + return _runningProjects.Clear(); + } - // reset capabilities: - _currentAggregateCapabilities = default; - } + projectsToRestart = projectPaths.SelectMany(path => _runningProjects.TryGetValue(path, out var array) ? array : []).ToImmutableArray(); + return runningProjectsByPath.RemoveRange(projectPaths); + }); // Do not terminate root process at this time - it would signal the cancellation token we are currently using. // The process will be restarted later on. var projectsToTerminate = projectsToRestart.Where(p => !p.Options.IsRootProject); // wait for all processes to exit to release their resources, so we can rebuild: - await TerminateAndDisposeRunningProjects(projectsToTerminate); + _ = await TerminateRunningProjects(projectsToTerminate, cancellationToken); return projectsToRestart; } + /// + /// Terminates process of the given . + /// Removes corresponding entries from . + /// + /// Should not be called with the root project. + /// + /// Exit code of the terminated process. + internal async ValueTask TerminateNonRootProcessAsync(RunningProject project, CancellationToken cancellationToken) + { + Debug.Assert(!project.Options.IsRootProject); + + var projectPath = project.ProjectNode.ProjectInstance.FullPath; + + UpdateRunningProjects(runningProjectsByPath => + { + if (!runningProjectsByPath.TryGetValue(projectPath, out var runningProjects) || + runningProjects.Remove(project) is var updatedRunningProjects && runningProjects == updatedRunningProjects) + { + _reporter.Verbose($"Ignoring an attempt to terminate process {project.ProcessId} of project '{projectPath}' that has no associated running processes."); + return runningProjectsByPath; + } + + return updatedRunningProjects is [] + ? runningProjectsByPath.Remove(projectPath) + : runningProjectsByPath.SetItem(projectPath, updatedRunningProjects); + }); + + // wait for all processes to exit to release their resources: + return (await TerminateRunningProjects([project], cancellationToken)).Single(); + } + + private void UpdateRunningProjects(Func>, ImmutableDictionary>> updater) + { + lock (_runningProjectsAndUpdatesGuard) + { + _runningProjects = updater(_runningProjects); + + // reset capabilities: + _currentAggregateCapabilities = default; + } + } + + private static async ValueTask> TerminateRunningProjects(IEnumerable projects, CancellationToken cancellationToken) + { + // cancel first, this will cause the process tasks to complete: + foreach (var project in projects) + { + project.ProcessTerminationSource.Cancel(); + } + + // wait for all tasks to complete: + return await Task.WhenAll(projects.Select(p => p.RunningProcess)).WaitAsync(cancellationToken); + } + private static Task ForEachProjectAsync(ImmutableDictionary> projects, Func action, CancellationToken cancellationToken) => Task.WhenAll(projects.SelectMany(entry => entry.Value).Select(project => action(project, cancellationToken))).WaitAsync(cancellationToken); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs index ca0fe1297410..0bdceee705ca 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs @@ -175,7 +175,7 @@ private async Task ReceiveApplyUpdateResult(CancellationToken cancellation private void DisposePipe() { - Reporter.Verbose("Disposing pipe"); + Reporter.Verbose("Disposing agent communication pipe"); _pipe?.Dispose(); _pipe = null; } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs index 08bdf0eee6d7..375e2a9b3248 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs @@ -9,5 +9,10 @@ namespace Microsoft.DotNet.Watcher; /// internal interface IRuntimeProcessLauncher : IAsyncDisposable { - ValueTask> GetEnvironmentVariablesAsync(CancellationToken cancelationToken); + IEnumerable<(string name, string value)> GetEnvironmentVariables(); + + /// + /// Initiates shutdown. Terminates all created processes. + /// + ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancellationToken); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs index 5020cbb03c69..9185f32044c2 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs @@ -3,11 +3,14 @@ using System.Globalization; using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher; +internal delegate ValueTask ProcessExitAction(int processId, int? exitCode); + internal sealed class ProjectLauncher( DotNetWatchContext context, ProjectNodeMap projectMap, @@ -23,7 +26,13 @@ public IReporter Reporter public EnvironmentOptions EnvironmentOptions => context.EnvironmentOptions; - public async ValueTask TryLaunchProcessAsync(ProjectOptions projectOptions, CancellationTokenSource processTerminationSource, bool build, CancellationToken cancellationToken) + public async ValueTask TryLaunchProcessAsync( + ProjectOptions projectOptions, + CancellationTokenSource processTerminationSource, + Action? onOutput, + RestartOperation restartOperation, + bool build, + CancellationToken cancellationToken) { var projectNode = projectMap.TryGetProjectNode(projectOptions.ProjectPath, projectOptions.TargetFramework); if (projectNode == null) @@ -34,45 +43,20 @@ public EnvironmentOptions EnvironmentOptions if (!projectNode.IsNetCoreApp(Versions.Version6_0)) { - Reporter.Error($"Hot Reload based watching is only supported in .NET 6.0 or newer apps. Update the project's launchSettings.json to disable this feature."); - return null; - } - - try - { - return await LaunchProcessAsync(projectOptions, projectNode, processTerminationSource, build, cancellationToken); - } - catch (ObjectDisposedException e) when (e.ObjectName == typeof(HotReloadDotNetWatcher).FullName) - { - Reporter.Verbose("Unable to launch project, watcher has been disposed"); + Reporter.Error($"Hot Reload based watching is only supported in .NET 6.0 or newer apps. Use --no-hot-reload switch or update the project's launchSettings.json to disable this feature."); return null; } - } - public async Task LaunchProcessAsync(ProjectOptions projectOptions, ProjectGraphNode projectNode, CancellationTokenSource processTerminationSource, bool build, CancellationToken cancellationToken) - { var processSpec = new ProcessSpec { Executable = EnvironmentOptions.MuxerPath, WorkingDirectory = projectOptions.WorkingDirectory, + OnOutput = onOutput, Arguments = build || projectOptions.Command is not ("run" or "test") ? [projectOptions.Command, .. projectOptions.CommandArguments] : [projectOptions.Command, "--no-build", .. projectOptions.CommandArguments] }; - // allow tests to watch for application output: - if (Reporter.ReportProcessOutput) - { - var projectPath = projectNode.ProjectInstance.FullPath; - processSpec.OnOutput += (sender, args) => - { - if (args.Data != null) - { - Reporter.ProcessOutput(projectPath, args.Data); - } - }; - } - var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment(); var namedPipeName = Guid.NewGuid().ToString(); @@ -100,10 +84,14 @@ public async Task LaunchProcessAsync(ProjectOptions projectOptio environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatch, "1"); environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchIteration, (Iteration + 1).ToString(CultureInfo.InvariantCulture)); - if (context.Options.Verbose) - { - environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1"); - } + // Do not ask agent to log to stdout until https://github.com/dotnet/sdk/issues/40484 is fixed. + // For now we need to set the env variable explicitly when we need to diagnose issue with the agent. + // Build targets might launch a process and read it's stdout. If the agent is loaded into such process and starts logging + // to stdout it might interfere with the expected output. + //if (context.Options.Verbose) + //{ + // environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1"); + //} // TODO: workaround for https://github.com/dotnet/sdk/issues/40484 var targetPath = projectNode.ProjectInstance.GetPropertyValue("RunCommand"); @@ -113,7 +101,7 @@ public async Task LaunchProcessAsync(ProjectOptions projectOptio var browserRefreshServer = await browserConnector.LaunchOrRefreshBrowserAsync(projectNode, processSpec, environmentBuilder, projectOptions, cancellationToken); environmentBuilder.ConfigureProcess(processSpec); - var processReporter = new MessagePrefixingReporter($"[{projectNode.GetDisplayName()}] ", Reporter); + var processReporter = new ProjectSpecificReporter(projectNode, Reporter); return await compilationHandler.TrackRunningProjectAsync( projectNode, @@ -121,11 +109,12 @@ public async Task LaunchProcessAsync(ProjectOptions projectOptio namedPipeName, browserRefreshServer, processSpec, + restartOperation, processReporter, processTerminationSource, cancellationToken); } - public ValueTask> TerminateProcessesAsync(IReadOnlyList projectPaths, CancellationToken cancellationToken) - => compilationHandler.TerminateNonRootProcessesAsync(projectPaths, cancellationToken); + public ValueTask TerminateProcessAsync(RunningProject project, CancellationToken cancellationToken) + => compilationHandler.TerminateNonRootProcessAsync(project, cancellationToken); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs b/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs index 6b4cad493400..99b04b828a63 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs @@ -8,15 +8,19 @@ namespace Microsoft.DotNet.Watcher.Tools { + internal delegate ValueTask RestartOperation(bool build, CancellationToken cancellationToken); + internal sealed class RunningProject( ProjectGraphNode projectNode, ProjectOptions options, DeltaApplier deltaApplier, IReporter reporter, BrowserRefreshServer? browserRefreshServer, - Task runningProcess, + Task runningProcess, + int processId, CancellationTokenSource processExitedSource, CancellationTokenSource processTerminationSource, + RestartOperation restartOperation, IReadOnlyList disposables, Task> capabilityProvider) : IDisposable { @@ -26,7 +30,9 @@ internal sealed class RunningProject( public readonly DeltaApplier DeltaApplier = deltaApplier; public readonly Task> CapabilityProvider = capabilityProvider; public readonly IReporter Reporter = reporter; - public readonly Task RunningProcess = runningProcess; + public readonly Task RunningProcess = runningProcess; + public readonly int ProcessId = processId; + public readonly RestartOperation RestartOperation = restartOperation; /// /// Cancellation source triggered when the process exits. diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs index da4f09764958..3ca446c43b30 100644 --- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs @@ -35,8 +35,6 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, MSBu public override async Task WatchAsync(CancellationToken shutdownCancellationToken) { - Debug.Assert(Context.ProjectGraph != null); - CancellationTokenSource? forceRestartCancellationSource = null; var hotReloadEnabledMessage = "Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload."; @@ -75,37 +73,61 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke EvaluationResult? evaluationResult = null; RunningProject? rootRunningProject = null; Task? fileSetWatcherTask = null; + IRuntimeProcessLauncher? runtimeProcessLauncher = null; + CompilationHandler? compilationHandler = null; try { + var rootProjectOptions = Context.RootProjectOptions; + var runtimeProcessLauncherFactory = _runtimeProcessLauncherFactory; + // Evaluate the target to find out the set of files to watch. // In case the app fails to start due to build or other error we can wait for these files to change. evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken); + Debug.Assert(evaluationResult.ProjectGraph != null); + + var rootProject = evaluationResult.ProjectGraph.GraphRoots.Single(); + + // use normalized MSBuild path so that we can index into the ProjectGraph + rootProjectOptions = rootProjectOptions with { ProjectPath = rootProject.ProjectInstance.FullPath }; + + if (rootProject.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability)) + { + runtimeProcessLauncherFactory ??= AspireServiceFactory.Instance; + Context.Reporter.Verbose("Using Aspire process launcher."); + } await using var browserConnector = new BrowserConnector(Context); - var projectMap = new ProjectNodeMap(Context.ProjectGraph, Context.Reporter); - await using var compilationHandler = new CompilationHandler(Context.Reporter); + var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter); + compilationHandler = new CompilationHandler(Context.Reporter); var staticFileHandler = new StaticFileHandler(Context.Reporter, projectMap, browserConnector); var scopedCssFileHandler = new ScopedCssFileHandler(Context.Reporter, projectMap, browserConnector); var projectLauncher = new ProjectLauncher(Context, projectMap, browserConnector, compilationHandler, iteration); - var rootProjectOptions = Context.RootProjectOptions; - var rootProjectNode = Context.ProjectGraph.GraphRoots.Single(); + var rootProjectNode = evaluationResult.ProjectGraph.GraphRoots.Single(); - await using var runtimeProcessLauncher = _runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions.BuildProperties); + runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions.BuildProperties); if (runtimeProcessLauncher != null) { - var launcherEnvironment = await runtimeProcessLauncher.GetEnvironmentVariablesAsync(iterationCancellationToken); + var launcherEnvironment = runtimeProcessLauncher.GetEnvironmentVariables(); rootProjectOptions = rootProjectOptions with { LaunchEnvironmentVariables = [.. rootProjectOptions.LaunchEnvironmentVariables, .. launcherEnvironment] }; } - rootRunningProject = await projectLauncher.TryLaunchProcessAsync(rootProjectOptions, rootProcessTerminationSource, build: true, iterationCancellationToken); + rootRunningProject = await projectLauncher.TryLaunchProcessAsync( + rootProjectOptions, + rootProcessTerminationSource, + onOutput: null, + restartOperation: new RestartOperation((_, _) => throw new InvalidOperationException("Root project shouldn't be restarted")), + build: true, + iterationCancellationToken); + if (rootRunningProject == null) { // error has been reported: + waitForFileChangeBeforeRestarting = false; return; } @@ -257,6 +279,11 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke else { Context.Reporter.Verbose("Restarting without prompt since dotnet-watch is running in non-interactive mode."); + + foreach (var project in projects) + { + Context.Reporter.Verbose($" Project to restart: '{project.Name}'"); + } } }, iterationCancellationToken); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); @@ -280,7 +307,8 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke await Task.WhenAll( projectsToRestart.Select(async runningProject => { - var newRunningProject = await projectLauncher.LaunchProcessAsync(runningProject.Options, runningProject.ProjectNode, new CancellationTokenSource(), build: true, shutdownCancellationToken); + var newRunningProject = await runningProject.RestartOperation(build: true, shutdownCancellationToken); + runningProject.Dispose(); await newRunningProject.WaitForProcessRunningAsync(shutdownCancellationToken); })) .WaitAsync(shutdownCancellationToken); @@ -304,10 +332,23 @@ await Task.WhenAll( rootProcessTerminationSource.Cancel(); } + if (runtimeProcessLauncher != null) + { + // Request cleanup of all processes created by the launcher before we terminate the root process. + // Non-cancellable - can only be aborted by forced Ctrl+C, which immediately kills the dotnet-watch process. + await runtimeProcessLauncher.TerminateLaunchedProcessesAsync(CancellationToken.None); + } + + if (compilationHandler != null) + { + // Non-cancellable - can only be aborted by forced Ctrl+C, which immediately kills the dotnet-watch process. + await compilationHandler.TerminateNonRootProcessesAndDispose(CancellationToken.None); + } + try { - // Wait for the root process to exit. Child processes will be terminated upon CompilationHandler disposal. - await Task.WhenAll(new[] { rootRunningProject?.RunningProcess, fileSetWatcherTask }.Where(t => t != null)!); + // Wait for the root process to exit. + await Task.WhenAll(new[] { (Task?)rootRunningProject?.RunningProcess, fileSetWatcherTask }.Where(t => t != null)!); } catch (OperationCanceledException) when (!shutdownCancellationToken.IsCancellationRequested) { @@ -316,6 +357,12 @@ await Task.WhenAll( finally { fileSetWatcherTask = null; + + if (runtimeProcessLauncher != null) + { + await runtimeProcessLauncher.DisposeAsync(); + } + rootRunningProject?.Dispose(); if (evaluationResult != null && @@ -381,9 +428,10 @@ private async ValueTask EvaluateRootProjectAsync(CancellationT { cancellationToken.ThrowIfCancellationRequested(); - var result = await RootFileSetFactory.TryCreateAsync(cancellationToken); + var result = await RootFileSetFactory.TryCreateAsync(requireProjectGraph: true, cancellationToken); if (result != null) { + Debug.Assert(result.ProjectGraph != null); return result; } diff --git a/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs index 61beec777935..0d1b8be10797 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs @@ -1,8 +1,8 @@ // 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; -using Microsoft.Build.Tasks; +using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.Extensions.Tools.Internal { @@ -18,10 +18,13 @@ internal sealed class ConsoleReporter(IConsole console, bool verbose, bool quiet private readonly object _writeLock = new(); - public bool ReportProcessOutput + public bool EnableProcessOutputReporting => false; - public void ProcessOutput(string projectPath, string data) + public void ReportProcessOutput(OutputLine line) + => throw new InvalidOperationException(); + + public void ReportProcessOutput(ProjectGraphNode project, OutputLine line) => throw new InvalidOperationException(); private void WriteLine(TextWriter writer, string message, ConsoleColor? color, string emoji) diff --git a/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs index 68bd365e4502..892d21e5b2e0 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs @@ -3,8 +3,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Graph; using Microsoft.Build.Tasks; using Microsoft.DotNet.Watcher; +using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.Extensions.Tools.Internal { @@ -28,12 +30,24 @@ public bool HasMessage [MemberNotNullWhen(true, nameof(Format), nameof(Emoji))] public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] out string? message) { + // Messages without Id are created by IReporter.Verbose|Output|Warn|Error helpers. + // They do not have arguments and we shouldn't interpret Format as a string with holes. + // Eventually, all messages should have a descriptor (so we can localize them) and this can be removed. + if (Id == null) + { + Debug.Assert(args is null or []); + Debug.Assert(HasMessage); + message = prefix + Format; + return true; + } + if (!HasMessage) { message = null; return false; } + message = prefix + string.Format(Format, args); return true; } @@ -67,16 +81,18 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou internal interface IReporter { void Report(MessageDescriptor descriptor, string prefix, object?[] args); - void ProcessOutput(string projectPath, string data); public bool IsVerbose => false; /// - /// True to call when launched process writes to standard output. + /// True to call when launched process writes to standard output. /// Used for testing. /// - bool ReportProcessOutput { get; } + bool EnableProcessOutputReporting { get; } + + void ReportProcessOutput(OutputLine line); + void ReportProcessOutput(ProjectGraphNode project, OutputLine line); void Report(MessageDescriptor descriptor, params object?[] args) => Report(descriptor, prefix: "", args); diff --git a/src/BuiltInTools/dotnet-watch/Internal/MessagePrefixingReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/MessagePrefixingReporter.cs deleted file mode 100644 index 5aa93ba59f34..000000000000 --- a/src/BuiltInTools/dotnet-watch/Internal/MessagePrefixingReporter.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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.Tools.Internal; - -namespace Microsoft.DotNet.Watcher; - -internal sealed class MessagePrefixingReporter(string additionalPrefix, IReporter underlyingReporter) : IReporter -{ - public bool IsVerbose - => underlyingReporter.IsVerbose; - - public bool ReportProcessOutput - => underlyingReporter.ReportProcessOutput; - - public void ProcessOutput(string projectPath, string data) - => underlyingReporter.ProcessOutput(projectPath, data); - - public void Report(MessageDescriptor descriptor, string prefix, object?[] args) - => underlyingReporter.Report(descriptor, additionalPrefix + prefix, args); -} diff --git a/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs index 2e294b0b62f5..85405a76f4a7 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs @@ -1,9 +1,9 @@ -// 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.Diagnostics; using System.Text.Json; +using Microsoft.Build.Graph; using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; @@ -21,62 +21,42 @@ namespace Microsoft.DotNet.Watcher.Tools internal class MSBuildFileSetFactory( string rootProjectFile, string? targetFramework, - IReadOnlyList<(string, string)>? buildProperties, + IReadOnlyList<(string name, string value)> buildProperties, EnvironmentOptions environmentOptions, - IReporter reporter, - OutputSink? outputSink, - bool trace) + IReporter reporter) { private const string TargetName = "GenerateWatchList"; private const string WatchTargetsFileName = "DotNetWatch.targets"; - private readonly OutputSink _outputSink = outputSink ?? new OutputSink(); - private readonly IReadOnlyList _buildFlags = InitializeArgs(FindTargetsFile(), targetFramework, buildProperties, trace); - public string RootProjectFile => rootProjectFile; // Virtual for testing. - public virtual async ValueTask TryCreateAsync(CancellationToken cancellationToken) + public virtual async ValueTask TryCreateAsync(bool? requireProjectGraph, CancellationToken cancellationToken) { var watchList = Path.GetTempFileName(); try { var projectDir = Path.GetDirectoryName(rootProjectFile); - - var capture = _outputSink.StartCapture(); - var arguments = new List - { - "msbuild", - "/nologo", - rootProjectFile, - $"/p:_DotNetWatchListFile={watchList}", - }; - -#if !DEBUG - if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest)) -#endif - { - arguments.Add("/bl"); - } - - if (environmentOptions.SuppressHandlingStaticContentFiles) - { - arguments.Add("/p:DotNetWatchContentFiles=false"); - } - - arguments.AddRange(_buildFlags); + var arguments = GetMSBuildArguments(watchList); + var capturedOutput = new List(); var processSpec = new ProcessSpec { Executable = environmentOptions.MuxerPath, WorkingDirectory = projectDir, Arguments = arguments, - OutputCapture = capture + OnOutput = line => + { + lock (capturedOutput) + { + capturedOutput.Add(line); + } + } }; reporter.Verbose($"Running MSBuild target '{TargetName}' on '{rootProjectFile}'"); - var exitCode = await ProcessRunner.RunAsync(processSpec, reporter, isUserApplication: false, processExitedSource: null, cancellationToken); + var exitCode = await ProcessRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken); if (exitCode != 0 || !File.Exists(watchList)) { @@ -85,9 +65,17 @@ internal class MSBuildFileSetFactory( reporter.Output($"MSBuild output from target '{TargetName}':"); reporter.Output(string.Empty); - foreach (var line in capture.Lines) + foreach (var (line, isError) in capturedOutput) { - reporter.Output($" {line}"); + var message = " " + line; + if (isError) + { + reporter.Error(message); + } + else + { + reporter.Output(message); + } } reporter.Output(string.Empty); @@ -142,7 +130,18 @@ void AddFile(string filePath, string? staticWebAssetPath) Debug.Assert(fileItems.Values.All(f => Path.IsPathRooted(f.FilePath)), "All files should be rooted paths"); #endif - return new EvaluationResult(fileItems); + // Load the project graph after the project has been restored: + ProjectGraph? projectGraph = null; + if (requireProjectGraph != null) + { + projectGraph = TryLoadProjectGraph(requireProjectGraph.Value); + if (projectGraph == null && requireProjectGraph == true) + { + return null; + } + } + + return new EvaluationResult(fileItems, projectGraph); } finally { @@ -150,36 +149,49 @@ void AddFile(string filePath, string? staticWebAssetPath) } } - private static IReadOnlyList InitializeArgs(string watchTargetsFile, string? targetFramework, IReadOnlyList<(string name, string value)>? buildProperties, bool trace) + private IReadOnlyList GetMSBuildArguments(string watchListFilePath) { - var args = new List + var watchTargetsFile = FindTargetsFile(); + + var arguments = new List { + "msbuild", + "/restore", "/nologo", - "/v:n", - "/t:" + TargetName, - "/p:DotNetWatchBuild=true", // extensibility point for users - "/p:DesignTimeBuild=true", // don't do expensive things - "/p:CustomAfterMicrosoftCommonTargets=" + watchTargetsFile, - "/p:CustomAfterMicrosoftCommonCrossTargetingTargets=" + watchTargetsFile, + "/v:m", + rootProjectFile, + "/t:" + TargetName }; - if (targetFramework != null) +#if !DEBUG + if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest)) +#endif { - args.Add("/p:TargetFramework=" + targetFramework); + arguments.Add("/bl:DotnetWatch.GenerateWatchList.binlog"); } - if (buildProperties != null) + arguments.AddRange(buildProperties.Select(p => $"/p:{p.name}={p.value}")); + + // Set dotnet-watch reserved properties after the user specified propeties, + // so that the former take precedence. + + if (environmentOptions.SuppressHandlingStaticContentFiles) { - args.AddRange(buildProperties.Select(p => $"/p:{p.name}={p.value}")); + arguments.Add("/p:DotNetWatchContentFiles=false"); } - if (trace) + if (targetFramework != null) { - // enables capturing markers to know which projects have been visited - args.Add("/p:_DotNetWatchTraceOutput=true"); + arguments.Add("/p:TargetFramework=" + targetFramework); } - return args; + arguments.Add("/p:_DotNetWatchListFile=" + watchListFilePath); + arguments.Add("/p:DotNetWatchBuild=true"); // extensibility point for users + arguments.Add("/p:DesignTimeBuild=true"); // don't do expensive things + arguments.Add("/p:CustomAfterMicrosoftCommonTargets=" + watchTargetsFile); + arguments.Add("/p:CustomAfterMicrosoftCommonCrossTargetingTargets=" + watchTargetsFile); + + return arguments; } private static string FindTargetsFile() @@ -198,5 +210,55 @@ private static string FindTargetsFile() var targetPath = searchPaths.Select(p => Path.Combine(p, WatchTargetsFileName)).FirstOrDefault(File.Exists); return targetPath ?? throw new FileNotFoundException("Fatal error: could not find DotNetWatch.targets"); } + + // internal for testing + internal ProjectGraph? TryLoadProjectGraph(bool projectGraphRequired) + { + var globalOptions = new Dictionary(); + if (targetFramework != null) + { + globalOptions.Add("TargetFramework", targetFramework); + } + + foreach (var (name, value) in buildProperties) + { + globalOptions[name] = value; + } + + try + { + return new ProjectGraph(rootProjectFile, globalOptions); + } + catch (Exception e) + { + reporter.Verbose("Failed to load project graph."); + + if (e is AggregateException { InnerExceptions: var innerExceptions }) + { + foreach (var inner in innerExceptions) + { + Report(inner); + } + } + else + { + Report(e); + } + + void Report(Exception e) + { + if (projectGraphRequired) + { + reporter.Error(e.Message); + } + else + { + reporter.Warn(e.Message); + } + } + } + + return null; + } } } diff --git a/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs index fc5f95a7d3c8..6812973c3b80 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs @@ -1,6 +1,9 @@ // 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.DotNet.Watcher.Internal; + namespace Microsoft.Extensions.Tools.Internal { /// @@ -14,9 +17,15 @@ private NullReporter() public static IReporter Singleton { get; } = new NullReporter(); - public bool ReportProcessOutput => false; + public bool EnableProcessOutputReporting + => false; + + public void ReportProcessOutput(OutputLine line) + => throw new InvalidOperationException(); + + public void ReportProcessOutput(ProjectGraphNode project, OutputLine line) + => throw new InvalidOperationException(); - public void ProcessOutput(string projectPath, string data) => throw new InvalidOperationException(); public void Report(MessageDescriptor descriptor, string prefix, object?[] args) { diff --git a/src/BuiltInTools/dotnet-watch/Internal/OutputCapture.cs b/src/BuiltInTools/dotnet-watch/Internal/OutputCapture.cs deleted file mode 100644 index 73b1995bc736..000000000000 --- a/src/BuiltInTools/dotnet-watch/Internal/OutputCapture.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.DotNet.Watcher.Internal -{ - internal sealed class OutputCapture - { - private readonly List _lines = new(); - public IEnumerable Lines => _lines; - public void AddLine(string line) => _lines.Add(line); - } -} diff --git a/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs b/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs new file mode 100644 index 000000000000..f80037321819 --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watcher.Internal; + +internal readonly record struct OutputLine(string Content, bool IsError); diff --git a/src/BuiltInTools/dotnet-watch/Internal/OutputSink.cs b/src/BuiltInTools/dotnet-watch/Internal/OutputSink.cs deleted file mode 100644 index d625dbb6899f..000000000000 --- a/src/BuiltInTools/dotnet-watch/Internal/OutputSink.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.DotNet.Watcher.Internal -{ - internal sealed class OutputSink - { - public OutputCapture? Current { get; private set; } - public OutputCapture StartCapture() - { - return (Current = new OutputCapture()); - } - } -} diff --git a/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs b/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs index d3e07e8bd62e..06d9fd194327 100644 --- a/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs +++ b/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs @@ -9,141 +9,166 @@ namespace Microsoft.DotNet.Watcher.Internal { internal sealed class ProcessRunner { + private const int SIGKILL = 9; + private const int SIGTERM = 15; + + private sealed class ProcessState + { + public int ProcessId; + public bool HasExited; + public bool ForceExit; + } + /// /// Launches a process. /// /// True if the process is a user application, false if it is a helper process (e.g. msbuild). - public static async Task RunAsync(ProcessSpec processSpec, IReporter reporter, bool isUserApplication, CancellationTokenSource? processExitedSource, CancellationToken processTerminationToken) + public static async Task RunAsync(ProcessSpec processSpec, IReporter reporter, bool isUserApplication, ProcessLaunchResult? launchResult, CancellationToken processTerminationToken) { Ensure.NotNull(processSpec, nameof(processSpec)); + var state = new ProcessState(); var stopwatch = new Stopwatch(); - using var process = CreateProcess(processSpec); - using var processState = new ProcessState(process, reporter); + var onOutput = processSpec.OnOutput; - processTerminationToken.Register(() => processState.TryKill()); - - var readOutput = false; - var readError = false; - if (processSpec.IsOutputCaptured) + // allow tests to watch for application output: + if (reporter.EnableProcessOutputReporting) { - readOutput = true; - readError = true; + onOutput += line => reporter.ReportProcessOutput(line); + } - process.OutputDataReceived += (_, a) => - { - if (!string.IsNullOrEmpty(a.Data)) - { - processSpec.OutputCapture.AddLine(a.Data); - } - }; + using var process = CreateProcess(processSpec, onOutput, state, reporter); - process.ErrorDataReceived += (_, a) => - { - if (!string.IsNullOrEmpty(a.Data)) - { - processSpec.OutputCapture.AddLine(a.Data); - } - }; - } - else if (processSpec.OnOutput != null) - { - readOutput = true; - process.OutputDataReceived += processSpec.OnOutput; - } + processTerminationToken.Register(() => TerminateProcess(process, state, reporter)); stopwatch.Start(); - int? processId = null; + Exception? launchException = null; try { - if (process.Start()) + if (!process.Start()) { - processId = process.Id; + throw new InvalidOperationException("Process can't be started."); } - } - finally - { - var argsDisplay = processSpec.GetArgumentsDisplay(); - if (processId.HasValue) - { - reporter.Report(MessageDescriptor.LaunchedProcess, processSpec.Executable, argsDisplay, processId.Value); - } - else + state.ProcessId = process.Id; + + if (onOutput != null) { - reporter.Error($"Failed to launch '{processSpec.Executable}' with arguments '{argsDisplay}'"); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); } } + catch (Exception e) + { + launchException = e; + } - if (readOutput) + var argsDisplay = processSpec.GetArgumentsDisplay(); + if (launchException == null) + { + reporter.Report(MessageDescriptor.LaunchedProcess, processSpec.Executable, argsDisplay, state.ProcessId); + } + else { - process.BeginOutputReadLine(); + reporter.Error($"Failed to launch '{processSpec.Executable}' with arguments '{argsDisplay}': {launchException.Message}"); + return int.MinValue; } - if (readError) + if (launchResult != null) { - process.BeginErrorReadLine(); + launchResult.ProcessId = process.Id; } int? exitCode = null; - var failed = false; try { - await processState.Task; + try + { + await process.WaitForExitAsync(processTerminationToken); + } + catch (OperationCanceledException) + { + // Process termination requested via cancellation token. + // Wait for the actual process exit. + while (true) + { + try + { + // non-cancellable to not leave orphaned processes around blocking resources: + await process.WaitForExitAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None); + break; + } + catch (TimeoutException) + { + // nop + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || state.ForceExit) + { + reporter.Output($"Waiting for process {state.ProcessId} to exit ..."); + } + else + { + reporter.Output($"Forcing process {state.ProcessId} to exit ..."); + } + + state.ForceExit = true; + } + } } - catch (Exception e) when (e is not OperationCanceledException) + catch (Exception e) { - failed = true; - if (isUserApplication) { - reporter.Error($"Application failed to launch: {e.Message}"); + reporter.Error($"Application failed: {e.Message}"); } } finally { stopwatch.Stop(); - if (!failed && !processTerminationToken.IsCancellationRequested) + state.HasExited = true; + + try { - try + exitCode = process.ExitCode; + } + catch + { + exitCode = null; + } + + reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms and exited with exit code {exitCode}."); + + if (isUserApplication) + { + if (exitCode == 0) { - exitCode = process.ExitCode; + reporter.Output("Exited"); } - catch + else if (exitCode == null) { - exitCode = null; + reporter.Error("Exited with unknown error code"); } - - reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms."); - - if (isUserApplication) + else { - if (exitCode == 0) - { - reporter.Output("Exited"); - } - else if (exitCode == null) - { - reporter.Error("Exited with unknown error code"); - } - else - { - reporter.Error($"Exited with error code {exitCode}"); - } + reporter.Error($"Exited with error code {exitCode}"); } } - processExitedSource?.Cancel(); + if (processSpec.OnExit != null) + { + await processSpec.OnExit(state.ProcessId, exitCode); + } } return exitCode ?? int.MinValue; } - private static Process CreateProcess(ProcessSpec processSpec) + private static Process CreateProcess(ProcessSpec processSpec, Action? onOutput, ProcessState state, IReporter reporter) { var process = new Process { @@ -153,8 +178,8 @@ private static Process CreateProcess(ProcessSpec processSpec) FileName = processSpec.Executable, UseShellExecute = false, WorkingDirectory = processSpec.WorkingDirectory, - RedirectStandardOutput = processSpec.IsOutputCaptured || (processSpec.OnOutput != null), - RedirectStandardError = processSpec.IsOutputCaptured, + RedirectStandardOutput = onOutput != null, + RedirectStandardError = onOutput != null, } }; @@ -175,83 +200,77 @@ private static Process CreateProcess(ProcessSpec processSpec) process.StartInfo.Environment.Add(env.Key, env.Value); } - return process; - } - - private sealed class ProcessState : IDisposable - { - private readonly IReporter _reporter; - private readonly Process _process; - private readonly TaskCompletionSource _processExitedCompletionSource = new(); - private volatile bool _disposed; - - public readonly Task Task; - - public ProcessState(Process process, IReporter reporter) + if (onOutput != null) { - _reporter = reporter; - _process = process; - _process.Exited += OnExited; - Task = _processExitedCompletionSource.Task.ContinueWith(_ => + process.OutputDataReceived += (_, args) => { try { - // We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously - // this code used Process.Exited, which could result in us missing some output due to the ordering of - // events. - // - // See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_ - if (!_process.WaitForExit(int.MaxValue)) + if (args.Data != null) { - throw new TimeoutException(); + onOutput(new OutputLine(args.Data, IsError: false)); } + } + catch (Exception e) + { + reporter.Verbose($"Error reading stdout of process {state.ProcessId}: {e}"); + } + }; - _process.WaitForExit(); + process.ErrorDataReceived += (_, args) => + { + try + { + if (args.Data != null) + { + onOutput(new OutputLine(args.Data, IsError: true)); + } } - catch (InvalidOperationException) + catch (Exception e) { - // suppress if this throws if no process is associated with this object anymore. + reporter.Verbose($"Error reading stderr of process {state.ProcessId}: {e}"); } - }); + }; } - public void TryKill() + return process; + } + + private static void TerminateProcess(Process process, ProcessState state, IReporter reporter) + { + try { - if (_disposed) + if (!state.HasExited && !process.HasExited) { - return; - } + reporter.Report(MessageDescriptor.KillingProcess, state.ProcessId.ToString()); - try - { - if (!_process.HasExited) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _reporter.Report(MessageDescriptor.KillingProcess, _process.Id); - _process.Kill(entireProcessTree: true); + process.Kill(); } - } - catch (Exception ex) - { - _reporter.Verbose($"Error while killing process '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}': {ex.Message}"); -#if DEBUG - _reporter.Verbose(ex.ToString()); -#endif - } - } + else + { + [DllImport("libc", SetLastError = true, EntryPoint = "kill")] + static extern int sys_kill(int pid, int sig); - private void OnExited(object? sender, EventArgs args) - => _processExitedCompletionSource.TrySetResult(); + var result = sys_kill(state.ProcessId, state.ForceExit ? SIGKILL : SIGTERM); + if (result != 0) + { + var error = Marshal.GetLastPInvokeError(); + reporter.Verbose($"Error while sending SIGTERM to process {state.ProcessId}: {Marshal.GetPInvokeErrorMessage(error)} (code {error})."); + } + } - public void Dispose() - { - if (!_disposed) - { - TryKill(); - _disposed = true; - _process.Exited -= OnExited; - _process.Dispose(); + reporter.Verbose($"Process {state.ProcessId} killed."); } } + catch (Exception ex) + { + reporter.Verbose($"Error while killing process {state.ProcessId}: {ex.Message}"); +#if DEBUG + reporter.Verbose(ex.ToString()); +#endif + } } } } diff --git a/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs new file mode 100644 index 000000000000..a46b9d078904 --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs @@ -0,0 +1,28 @@ +// 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.DotNet.Watcher.Internal; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.Watcher; + +internal sealed class ProjectSpecificReporter(ProjectGraphNode node, IReporter underlyingReporter) : IReporter +{ + private readonly string _projectDisplayName = node.GetDisplayName(); + + public bool IsVerbose + => underlyingReporter.IsVerbose; + + public bool EnableProcessOutputReporting + => underlyingReporter.EnableProcessOutputReporting; + + public void ReportProcessOutput(ProjectGraphNode project, OutputLine line) + => underlyingReporter.ReportProcessOutput(project, line); + + public void ReportProcessOutput(OutputLine line) + => ReportProcessOutput(node, line); + + public void Report(MessageDescriptor descriptor, string prefix, object?[] args) + => underlyingReporter.Report(descriptor, $"[{_projectDisplayName}] {prefix}", args); +} diff --git a/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs b/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs new file mode 100644 index 000000000000..3c58c69946a9 --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watcher +{ + internal sealed class ProcessLaunchResult + { + public int? ProcessId { get; set; } + } +} diff --git a/src/BuiltInTools/dotnet-watch/ProcessSpec.cs b/src/BuiltInTools/dotnet-watch/ProcessSpec.cs index fbf4dfa74c0e..c6b651c91b55 100644 --- a/src/BuiltInTools/dotnet-watch/ProcessSpec.cs +++ b/src/BuiltInTools/dotnet-watch/ProcessSpec.cs @@ -1,9 +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; -using System.Diagnostics.CodeAnalysis; using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.DotNet.Watcher @@ -15,13 +12,10 @@ internal sealed class ProcessSpec public Dictionary EnvironmentVariables { get; } = new(); public IReadOnlyList? Arguments { get; set; } public string? EscapedArguments { get; set; } - public OutputCapture? OutputCapture { get; set; } - public DataReceivedEventHandler? OnOutput { get; set; } + public Action? OnOutput { get; set; } + public ProcessExitAction? OnExit { get; set; } public CancellationToken CancelOutputCapture { get; set; } - [MemberNotNullWhen(true, nameof(OutputCapture))] - public bool IsOutputCaptured => OutputCapture != null; - public string? ShortDisplayName() => Path.GetFileNameWithoutExtension(Executable); diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index d6cfaa5b212a..75106f6e67f8 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -97,27 +97,27 @@ public static async Task Main(string[] args) // internal for testing internal async Task RunAsync() { - var cancellationSource = new CancellationTokenSource(); - var cancellationToken = cancellationSource.Token; + var shutdownCancellationSource = new CancellationTokenSource(); + var shutdownCancellationToken = shutdownCancellationSource.Token; console.CancelKeyPress += OnCancelKeyPress; try { - if (cancellationToken.IsCancellationRequested) + if (shutdownCancellationToken.IsCancellationRequested) { return 1; } if (options.List) { - return await ListFilesAsync(cancellationToken); + return await ListFilesAsync(shutdownCancellationToken); } var watcher = CreateWatcher(runtimeProcessLauncherFactory: null); - await watcher.WatchAsync(cancellationToken); + await watcher.WatchAsync(shutdownCancellationToken); return 0; } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (shutdownCancellationToken.IsCancellationRequested) { // Ctrl+C forced an exit return 0; @@ -131,20 +131,24 @@ internal async Task RunAsync() finally { console.CancelKeyPress -= OnCancelKeyPress; - cancellationSource.Dispose(); + shutdownCancellationSource.Dispose(); } void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs args) { - // suppress CTRL+C on the first press - args.Cancel = !cancellationSource.IsCancellationRequested; + // if we already canceled, we force immediate shutdown: + var forceShutdown = shutdownCancellationSource.IsCancellationRequested; - if (args.Cancel) + if (!forceShutdown) { reporter.Report(MessageDescriptor.ShutdownRequested); + shutdownCancellationSource.Cancel(); + args.Cancel = true; + } + else + { + Environment.Exit(0); } - - cancellationSource.Cancel(); } } @@ -156,21 +160,12 @@ internal Watcher CreateWatcher(IRuntimeProcessLauncherFactory? runtimeProcessLau reporter.Output("Polling file watcher is enabled"); } - var projectGraph = TryReadProject(rootProjectOptions, reporter); - if (projectGraph != null) - { - // use normalized MSBuild path so that we can index into the ProjectGraph - rootProjectOptions = rootProjectOptions with { ProjectPath = projectGraph.GraphRoots.Single().ProjectInstance.FullPath }; - } - var fileSetFactory = new MSBuildFileSetFactory( rootProjectOptions.ProjectPath, rootProjectOptions.TargetFramework, rootProjectOptions.BuildProperties, environmentOptions, - reporter, - outputSink: null, - trace: true); + reporter); bool enableHotReload; if (rootProjectOptions.Command != "run") @@ -191,7 +186,6 @@ internal Watcher CreateWatcher(IRuntimeProcessLauncherFactory? runtimeProcessLau var context = new DotNetWatchContext { - ProjectGraph = projectGraph, Reporter = reporter, Options = options.GlobalOptions, EnvironmentOptions = environmentOptions, @@ -203,33 +197,6 @@ internal Watcher CreateWatcher(IRuntimeProcessLauncherFactory? runtimeProcessLau : new DotNetWatcher(context, fileSetFactory); } - // internal for testing - internal static ProjectGraph? TryReadProject(ProjectOptions options, IReporter reporter) - { - var globalOptions = new Dictionary(); - if (options.TargetFramework != null) - { - globalOptions.Add("TargetFramework", options.TargetFramework); - } - - foreach (var (name, value) in options.BuildProperties) - { - globalOptions[name] = value; - } - - try - { - return new ProjectGraph(options.ProjectPath, globalOptions); - } - catch (Exception ex) - { - reporter.Verbose("Reading the project instance failed."); - reporter.Verbose(ex.ToString()); - } - - return null; - } - private async Task ListFilesAsync(CancellationToken cancellationToken) { var fileSetFactory = new MSBuildFileSetFactory( @@ -237,11 +204,9 @@ private async Task ListFilesAsync(CancellationToken cancellationToken) rootProjectOptions.TargetFramework, rootProjectOptions.BuildProperties, environmentOptions, - reporter, - outputSink: null, - trace: false); + reporter); - if (await fileSetFactory.TryCreateAsync(cancellationToken) is not { } evaluationResult) + if (await fileSetFactory.TryCreateAsync(requireProjectGraph: null, cancellationToken) is not { } evaluationResult) { return 1; } diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json index 048a4fa86026..dafd8c0ab7ef 100644 --- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json +++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "dotnet-watch": { "commandName": "Project", - "commandLineArgs": "--verbose", + "commandLineArgs": "--verbose /bl:DotnetRun.binlog", "workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm", "environmentVariables": { "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)" diff --git a/src/BuiltInTools/dotnet-watch/Watcher.cs b/src/BuiltInTools/dotnet-watch/Watcher.cs index 743a25b627f4..b24a93f700e5 100644 --- a/src/BuiltInTools/dotnet-watch/Watcher.cs +++ b/src/BuiltInTools/dotnet-watch/Watcher.cs @@ -10,6 +10,6 @@ internal abstract class Watcher(DotNetWatchContext context, MSBuildFileSetFactor public DotNetWatchContext Context => context; public MSBuildFileSetFactory RootFileSetFactory => rootFileSetFactory; - public abstract Task WatchAsync(CancellationToken cancellationToken); + public abstract Task WatchAsync(CancellationToken shutdownCancellationToken); } } diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj index f405fff96f9b..299353a7fb71 100644 --- a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj +++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj @@ -1,4 +1,5 @@  + @@ -32,6 +33,7 @@ + diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/details/DetailsCommand.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/details/DetailsCommand.cs index 8a73099ea518..0f49518164f7 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/details/DetailsCommand.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/details/DetailsCommand.cs @@ -10,8 +10,6 @@ namespace Microsoft.TemplateEngine.Cli.Commands { internal class DetailsCommand : BaseCommand { - private static NugetApiManager _nugetApiManager = new(); - internal DetailsCommand( Func hostBuilder) : base(hostBuilder, "details", SymbolStrings.Command_Details_Description) @@ -52,7 +50,7 @@ protected override async Task ExecuteAsync( args.VersionCriteria, args.Interactive, args.AdditionalSources, - _nugetApiManager, + new NugetApiManager(), cancellationToken).ConfigureAwait(false); await CheckTemplatesWithSubCommandName(args, templatePackageManager, cancellationToken).ConfigureAwait(false); diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index 018f4718ae9f..1f2342e78bdd 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -348,7 +348,7 @@ private IEnumerable LoadOverrideSources(PackageSourceLocation pac private List LoadDefaultSources(PackageId packageId, PackageSourceLocation packageSourceLocation = null, PackageSourceMapping packageSourceMapping = null) { List defaultSources = new(); - string currentDirectory = Directory.GetCurrentDirectory(); + string currentDirectory = _currentWorkingDirectory ?? Directory.GetCurrentDirectory(); ISettings settings; if (packageSourceLocation?.NugetConfig != null) { diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1028/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1028/bundle.wxl index 9d5177d0e99b..fc6e58cd73c5 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1028/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1028/bundle.wxl @@ -4,7 +4,7 @@ 確定要取消嗎? 前一版 安裝說明 - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 安裝、修復、解除安裝 + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 安裝、修復、解除安裝 或在目錄中建立套件組合的完整本機複本。'/install' 是預設值。 /passive | /quiet - 顯示最基本的 UI 且不出現提示,或是不顯示任何 UI 且 diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1029/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1029/bundle.wxl index c381ddff74d5..6189bb220753 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1029/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1029/bundle.wxl @@ -4,7 +4,7 @@ Opravdu chcete akci zrušit? Předchozí verze Nápověda nastavení - /install | /repair | /uninstall | /layout [\[]"adresář"[\]] – Nainstaluje, opraví, odinstaluje + /install | /repair | /uninstall | /layout [\[]"adresář"[\]] – Nainstaluje, opraví, odinstaluje nebo vytvoří úplnou místní kopii sady v adresáři. Výchozí nastavení je /install. /passive | /quiet – Zobrazí minimální uživatelské rozhraní bez jakýchkoli výzev nebo nezobrazí žádné uživatelské rozhraní ani diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1031/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1031/bundle.wxl index 7de6e9ba6052..feb24bba7898 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1031/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1031/bundle.wxl @@ -4,7 +4,7 @@ Möchten Sie den Vorgang abbrechen? Vorherige Version Setup-Hilfe - /install | /repair | /uninstall | /layout [\[]„Verzeichnis“[\]] – installiert, repariert, deinstalliert + /install | /repair | /uninstall | /layout [\[]„Verzeichnis“[\]] – installiert, repariert, deinstalliert oder erstellt eine vollständige lokale Kopie des Bündels im Verzeichnis. „/install“ ist die Standardeinstellung. /passive | /quiet – Zeigt eine minimale Benutzeroberfläche ohne Eingabeaufforderungen an oder zeigt weder eine Benutzeroberfläche noch diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1036/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1036/bundle.wxl index f75f57e6aa2b..667dddd946d5 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1036/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1036/bundle.wxl @@ -4,7 +4,7 @@ Voulez-vous vraiment annuler ? Version précédente Aide du programme d'installation - /install | /repair | /uninstall | /layout [\[]"directory"[\]] : installe, répare, désinstalle + /install | /repair | /uninstall | /layout [\[]"directory"[\]] : installe, répare, désinstalle ou crée une copie locale complète de l’offre groupée dans le répertoire. '/install' est la valeur par défaut. /passive | /quiet – affiche une interface utilisateur minimale sans invites ou n’affiche ni interface utilisateur et diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1040/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1040/bundle.wxl index bb100ae4aea8..14fd33a2e7cd 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1040/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1040/bundle.wxl @@ -4,7 +4,7 @@ Annullare? Versione precedente Guida all'installazione - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - installa, ripristina, disinstalla + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - installa, ripristina, disinstalla oppure crea una copia locale completa del pacchetto nella directory. '/install' è l'impostazione predefinita. /passive | /quiet - consente di visualizzare un'interfaccia utente minima senza messaggi o di non visualizzare alcuna interfaccia utente diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1041/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1041/bundle.wxl index 059f00ef8344..cb009f405560 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1041/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1041/bundle.wxl @@ -4,7 +4,7 @@ 取り消しますか? 以前のバージョン セットアップのヘルプ - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - インストール、修復、アンインストール + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - インストール、修復、アンインストール または、バンドルの完全なローカル コピーをディレクトリに作成します。'/install' が既定値です。 /passive | /quiet - 最小限の UI をプロンプトなしで表示するか、UI と diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1042/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1042/bundle.wxl index 3835ff78b512..89c481c3f602 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1042/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1042/bundle.wxl @@ -4,7 +4,7 @@ 취소하시겠습니까? 이전 버전 설치 도움말 - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 설치, 복구, 제거 + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 설치, 복구, 제거 또는 디렉터리에 번들의 전체 로컬 복사본을 만듭니다. '/install'이 기본값입니다. /passive | /quiet - 프롬프트 없이 최소 UI를 표시하거나 UI 및 diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1045/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1045/bundle.wxl index bbc01b58d900..2cb469ae7d21 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1045/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1045/bundle.wxl @@ -4,7 +4,7 @@ Czy na pewno chcesz anulować? Poprzednia wersja Instalator — Pomoc - /install | /repair | /uninstall | /layout [\[]directory[\]] — instaluje, naprawia, odinstalowuje + /install | /repair | /uninstall | /layout [\[]directory[\]] — instaluje, naprawia, odinstalowuje lub tworzy pełną lokalną kopię pakietu w katalogu. „/install” jest wartością domyślną. /passive | /quiet — wyświetla minimalny interfejs użytkownika bez monitów lub nie wyświetla interfejsu użytkownika diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1046/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1046/bundle.wxl index 20dd08ef43b8..bdbb05f0fd48 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1046/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1046/bundle.wxl @@ -4,7 +4,7 @@ Tem certeza de que deseja cancelar? Versão anterior Ajuda da Instalação - /install | /repair | /uninstall | /layout [\[]"directory"[\]] – instala, repara, desinstala + /install | /repair | /uninstall | /layout [\[]"directory"[\]] – instala, repara, desinstala ou cria uma cópia local completa do pacote no diretório. "/install" é o padrão. /passive | /quiet – exibe a interface do usuário mínima sem prompts ou não exibe nenhuma interface do usuário e diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1049/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1049/bundle.wxl index 1ba8b4521428..18402ec4826b 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1049/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1049/bundle.wxl @@ -4,7 +4,7 @@ Отменить? Предыдущая версия Справка по установке - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - устанавливает, исправляет, удаляет + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - устанавливает, исправляет, удаляет или создает полную локальную копию пакета в каталоге. '/install' - значение по умолчанию. /passive | /quiet — отображает минимальный пользовательский интерфейс без запросов или не отображает пользовательский интерфейс и diff --git a/src/Installer/redist-installer/packaging/windows/LCID/1055/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/1055/bundle.wxl index 5f78c53ce22c..e42f5bcdbbe5 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/1055/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/1055/bundle.wxl @@ -4,7 +4,7 @@ İptal etmek istediğinizden emin misiniz? Önceki sürüm Kurulum Yardımı - /install | /repair | /uninstall | /layout [\[]"dizin"[\]] - yüklemeler, onarımlar, kaldırmalar + /install | /repair | /uninstall | /layout [\[]"dizin"[\]] - yüklemeler, onarımlar, kaldırmalar veya paketin tam bir yerel kopyasını dizinde oluşturur. '/install' varsayılandır. /passive | /quiet - kullanıcı arabirimini istem olmadan minimum düzeyde görüntüler veya kullanıcı arabirimi ve diff --git a/src/Installer/redist-installer/packaging/windows/LCID/2052/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/2052/bundle.wxl index 66d3d768f2ce..3cd07d2fac77 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/2052/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/2052/bundle.wxl @@ -4,7 +4,7 @@ 是否确实要取消? 上一版本 安装程序帮助 - /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 安装、修复、卸载 + /install | /repair | /uninstall | /layout [\[]"directory"[\]] - 安装、修复、卸载 或在目录中创建捆绑包的完整本地副本。'/install' 是默认值。 /passive | /quiet - 显示最小 UI 且无提示,或不显示 UI 和 diff --git a/src/Installer/redist-installer/packaging/windows/LCID/3082/bundle.wxl b/src/Installer/redist-installer/packaging/windows/LCID/3082/bundle.wxl index 966c9d647199..0bd39310edc7 100644 --- a/src/Installer/redist-installer/packaging/windows/LCID/3082/bundle.wxl +++ b/src/Installer/redist-installer/packaging/windows/LCID/3082/bundle.wxl @@ -4,7 +4,7 @@ ¿Está seguro de que desea cancelar la operación? Versión anterior Ayuda del programa de instalación - /install | /repair | /uninstall | /layout [\[]"directory"[\]]: instala, repara, desinstala + /install | /repair | /uninstall | /layout [\[]"directory"[\]]: instala, repara, desinstala o crea una copia local completa de la agrupación en el directorio. "/install" es el valor predeterminado. /passive | /quiet: muestra una interfaz de usuario mínima sin avisos o no muestra ninguna interfaz de usuario ni diff --git a/src/Installer/redist-installer/redist-installer.csproj b/src/Installer/redist-installer/redist-installer.csproj index 75de618eabe1..077ff2bf69cc 100644 --- a/src/Installer/redist-installer/redist-installer.csproj +++ b/src/Installer/redist-installer/redist-installer.csproj @@ -10,6 +10,7 @@ true true + true diff --git a/src/Installer/redist-installer/targets/GenerateLayout.targets b/src/Installer/redist-installer/targets/GenerateLayout.targets index 609731a3ec8f..bb05abdd8e56 100644 --- a/src/Installer/redist-installer/targets/GenerateLayout.targets +++ b/src/Installer/redist-installer/targets/GenerateLayout.targets @@ -9,11 +9,9 @@ - $(VSRedistCommonAspNetCoreSharedFrameworkx64100PackageVersion) - $(MicrosoftAspNetCoreAppRuntimePackageVersion) + $(MicrosoftAspNetCoreAppRefInternalPackageVersion) - $(VSRedistCommonNetCoreSharedFrameworkx64100PackageVersion) - $(MicrosoftNETCoreAppRuntimePackageVersion) + $(MicrosoftNETCorePlatformsPackageVersion) $(VSRedistCommonWindowsDesktopSharedFrameworkx64100PackageVersion) $(MicrosoftWindowsDesktopAppRuntimePackageVersion) diff --git a/src/Installer/redist-installer/targets/GenerateMSIs.targets b/src/Installer/redist-installer/targets/GenerateMSIs.targets index bb9d9f4b157f..49c8e1f86f48 100644 --- a/src/Installer/redist-installer/targets/GenerateMSIs.targets +++ b/src/Installer/redist-installer/targets/GenerateMSIs.targets @@ -275,24 +275,6 @@ Overwrite="true" /> - - - %(SDKInternalFiles.Identity) - - - - - - - - $(MinimumVSVersion.Substring(0,$(MinimumVSVersion.LastIndexOf('.')))) - $([MSBuild]::Add($(MinimumVSVersion), .1)) - - - + + diff --git a/src/Tasks/Common/Resources/Strings.resx b/src/Tasks/Common/Resources/Strings.resx index f7fa72d965ab..3bd60a9c395b 100644 --- a/src/Tasks/Common/Resources/Strings.resx +++ b/src/Tasks/Common/Resources/Strings.resx @@ -940,14 +940,7 @@ You may need to build the project on another operating system or architecture, o <IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', '{0}'))">true</IsTrimmable> {StrBegins="NETSDK1212: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - {StrBegins="NETSDK1213: "} - - - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - {StrBegins="NETSDK1214: "} - + NETSDK1215: Targeting .NET Standard prior to 2.0 is no longer recommended. See {0} for more details. {StrBegins="NETSDK1215: "} @@ -976,9 +969,18 @@ You may need to build the project on another operating system or architecture, o NETSDK1221: NuGetPackageRoot property is empty so package Microsoft.Net.Sdk.Compilers.Toolset cannot be used but it is recommended because your MSBuild and SDK versions are mismatched. Ensure you are building with '/restore /t:Build' and not '/t:Restore;Build'. {StrBegins="NETSDK1221: "}{Locked="NuGetPackageRoot"}{Locked="Microsoft.Net.Sdk.Compilers.Toolset"}{Locked="'/restore /t:Build'"}{Locked="'/t:Restore;Build'"} - - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. + + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. {StrBegins="NETSDK1222: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} + + + + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} + diff --git a/src/Tasks/Common/Resources/xlf/Strings.cs.xlf b/src/Tasks/Common/Resources/xlf/Strings.cs.xlf index 71b7502373bc..8fcea06dac8a 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.cs.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: Aktuální sada .NET SDK nepodporuje .NET Framework bez použití výchozích nastavení .NET SDK. Pravděpodobně došlo k neshodě mezi vlastnostmi CLRSupport projektu C++/CLI a TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Cílení na .NET 8.0 nebo vyšší se ve Visual Studiu 2022 17.7 nepodporuje. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 se vztahuje pouze na cíle .NET Framework. Nepodporuje se a nemá žádný vliv při cílení na .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 se vztahuje pouze na cíle .NET Framework. Nepodporuje se a nemá žádný vliv při cílení na .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.de.xlf b/src/Tasks/Common/Resources/xlf/Strings.de.xlf index da282ea1e8e7..2fa09f8d44b9 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.de.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: Das aktuelle .NET SDK unterstützt das .NET Framework nur, wenn .NET SDK-Standardwerte verwendet werden. Wahrscheinlich liegt ein Konflikt zwischen der CLRSupport-Eigenschaft des C++-/CLI-Projekts und TargetFramework vor. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Die Ausrichtung auf .NET 8.0 oder höher in Visual Studio 2022 17.7 wird nicht unterstützt. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 gilt nur für .NET Framework Ziele. Dies wird nicht unterstützt und hat keine Auswirkungen auf .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 gilt nur für .NET Framework Ziele. Dies wird nicht unterstützt und hat keine Auswirkungen auf .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.es.xlf b/src/Tasks/Common/Resources/xlf/Strings.es.xlf index 32eff79b1fb6..a7fb5d086be1 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.es.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: El SDK de .NET actual no admite .NET Framework sin usar los valores predeterminados de dicho SDK. Posiblemente se deba a la falta de coincidencia entre la propiedad CLRSupport del proyecto de C++/CLI y TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: no se admite el destino de .NET 8.0 o posterior en Visual Studio 2022 17.7. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 solo se aplica a destinos de .NET Framework. No se admite y no tiene efecto cuando el destino es .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 solo se aplica a destinos de .NET Framework. No se admite y no tiene efecto cuando el destino es .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.fr.xlf b/src/Tasks/Common/Resources/xlf/Strings.fr.xlf index f27b89db963c..d5012be61705 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.fr.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: Le SDK .NET actuel ne prend pas en charge le .NET Framework avec des valeurs du SDK .NET autres que celles par défaut. Cela est probablement dû à une incompatibilité entre la propriété CLRSupport du projet C++/CLI et TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: le ciblage de .NET 8.0 ou plus dans Visual Studio 2022 17.7 n’est pas pris en charge. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 s’applique uniquement aux cibles .NET Framework. Il n’est pas pris en charge et n’a aucun effet lors du ciblage de .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 s’applique uniquement aux cibles .NET Framework. Il n’est pas pris en charge et n’a aucun effet lors du ciblage de .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.it.xlf b/src/Tasks/Common/Resources/xlf/Strings.it.xlf index b64fc1d54718..8c8f32239c75 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.it.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: l'istanza corrente di .NET SDK non supporta .NET Framework senza usare le impostazioni predefinite di .NET SDK. Il problema dipende probabilmente da una mancata corrispondenza tra la proprietà CLRSupport del progetto C++/CLI e TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: la destinazione .NET 8.0 o versione successiva in Visual Studio 2022 17.7 non è supportata. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 si applica solo alle destinazioni .NET Framework. Non è supportato e non ha alcun effetto quando si usa .NET Core come destinazione. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 si applica solo alle destinazioni .NET Framework. Non è supportato e non ha alcun effetto quando si usa .NET Core come destinazione. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.ja.xlf b/src/Tasks/Common/Resources/xlf/Strings.ja.xlf index cbf1a9fc7304..7dcc1a1c66ae 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.ja.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: 現在の .NET SDK では、.NET SDK の既定値を使用せずに .NET Framework をサポートすることはできません。これは、C++/CLI プロジェクトの CLRSupport プロパティと TargetFramework の間の不一致が原因と考えられます。 {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Visual Studio 2022 17.7 では .NET 8.0 以上をターゲットにすることはできません。 - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 は、.NET Framework ターゲットにのみ適用されます。これはサポートされておらず、.NET Core をターゲットにする場合には効果がありません。 - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 は、.NET Framework ターゲットにのみ適用されます。これはサポートされておらず、.NET Core をターゲットにする場合には効果がありません。 + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.ko.xlf b/src/Tasks/Common/Resources/xlf/Strings.ko.xlf index 55dede12ac9b..b17c776ceeec 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.ko.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: 현재 .NET SDK는 .NET SDK 기본값을 사용하지 않는 .NET Framework를 지원하지 않습니다. C++/CLI 프로젝트 CLRSupport 속성과 TargetFramework 사이의 불일치 때문일 수 있습니다. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Visual Studio 2022 17.7에서는 .NET 8.0 이상을 대상으로 지정하는 것이 지원되지 않습니다. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64는 .NET Framework 대상에만 적용됩니다. .NET Core를 대상으로 하는 경우 지원되지 않으며 영향을 주지 않습니다. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64는 .NET Framework 대상에만 적용됩니다. .NET Core를 대상으로 하는 경우 지원되지 않으며 영향을 주지 않습니다. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.pl.xlf b/src/Tasks/Common/Resources/xlf/Strings.pl.xlf index ee25744e373b..963bbc6f47fc 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.pl.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: Bieżący zestaw .NET SDK nie obsługuje programu .NET Framework bez użycia wartości domyślnych zestawu .NET SDK. Prawdopodobna przyczyna to niezgodność między właściwością CLRSupport projektu C++/CLI i elementu TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: platforma docelowa .NET 8.0 lub nowsza w programie Visual Studio 2022 17.7 nie jest obsługiwana. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: element PreferNativeArm64 ma zastosowanie tylko do elementów docelowych programu .NET Framework. Nie jest ona obsługiwana i nie ma żadnego efektu w przypadku określania wartości docelowej platformy .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: element PreferNativeArm64 ma zastosowanie tylko do elementów docelowych programu .NET Framework. Nie jest ona obsługiwana i nie ma żadnego efektu w przypadku określania wartości docelowej platformy .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Common/Resources/xlf/Strings.pt-BR.xlf index b985dd5c1351..be20384f1f48 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.pt-BR.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: o SDK do .NET atual não dá suporte ao .NET Framework sem o uso de Padrões do SDK do .NET. O motivo é provavelmente uma incompatibilidade entre a propriedade CLRSupport do projeto C++/CLI e a TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: não há suporte para o direcionamento do .NET 8.0 ou superior no Visual Studio 2022 17.7. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 se aplica somente a destinos .NET Framework. Não há suporte para ele e não tem efeito ao direcionar o .NET Core. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 se aplica apenas a destinos do .NET Framework. Não há suporte e não tem efeito ao direcionar para o .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.ru.xlf b/src/Tasks/Common/Resources/xlf/Strings.ru.xlf index b2c499b154f1..71069896f43b 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.ru.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: The current .NET SDK does not support .NET Framework without using .NET SDK Defaults. It is likely due to a mismatch between C++/CLI project CLRSupport property and TargetFramework. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 применяется только к целевым объектам .NET Framework. Для целевых объектов .NET Core он не поддерживается и не оказывает влияния. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 применяется только к целевым объектам .NET Framework. Он не поддерживается и не работает для целевых объектов .NET Core. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.tr.xlf b/src/Tasks/Common/Resources/xlf/Strings.tr.xlf index fd5d62d087bc..6a70211cb13e 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.tr.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: Geçerli .NET SDK, .NET SDK Varsayılanlarını kullanmadan .NET Framework'ü desteklemiyor. C++/CLI projesi CLRSupport özelliği ve TargetFramework arasındaki uyuşmazlık bu duruma neden olabilir. {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: Visual Studio 2022 17.7'da .NET 8.0 veya daha üst sürümünü hedefleme desteklenmiyor. - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 yalnızca .NET Framework hedefleri için geçerlidir. .NET Core hedeflenirken desteklenmez ve herhangi bir etkisi yoktur. - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 yalnızca .NET Framework hedefleri için geçerlidir. .NET Core hedeflenirken desteklenmez ve herhangi bir etkisi yoktur. + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Common/Resources/xlf/Strings.zh-Hans.xlf index ae8198fa5a2b..ed429333a887 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.zh-Hans.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: 未使用 .NET SDK 默认设置的情况下,当前 .NET SDK 不支持 .NET Framework。很可能是因为 C++/CLI 项目的 CLRSupport 属性和 TargetFramework 之间存在不匹配情况。 {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: 不支持在 Visual Studio 2022 17.7 中以 .NET 8.0 或更高版本为目标。 - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 仅适用于 .NET Framework 目标。它不受支持,并且在面向 .NET Core 时不起作用。 - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 仅适用于 .NET Framework 目标。它不受支持,并且在以 .NET Core 为目标时不起作用。 + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Common/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Common/Resources/xlf/Strings.zh-Hant.xlf index c1a3e77b9493..9de539545262 100644 --- a/src/Tasks/Common/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Common/Resources/xlf/Strings.zh-Hant.xlf @@ -73,9 +73,9 @@ {StrBegins="NETSDK1079: "} - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - NETSDK1222: ASP.NET Core framework assets are not supported for the target framework. - {StrBegins="NETSDK1222: "} + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + NETSDK1224: ASP.NET Core framework assets are not supported for the target framework. + {StrBegins="NETSDK1224: "} NETSDK1080: A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference. @@ -646,10 +646,10 @@ The following are names of parameters or literal values and should not be transl NETSDK1115: 目前的 .NET SDK 不支援在不使用 .NET SDK 預設的情形下使用 .NET Framework。這可能是因為 C++/CLI 專案 CLRSupport 屬性與 TargetFramework 不相符所致。 {StrBegins="NETSDK1115: "} - - NETSDK1213: Targeting .NET 8.0 or higher in Visual Studio 2022 17.7 is not supported. - NETSDK1213: 不支援在 Visual Studio 2022 17.7 中以 .NET 8.0 或更高版本為目標。 - {StrBegins="NETSDK1213: "} + + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported. + {StrBegins="NETSDK1223: "} NETSDK1084: There is no application host available for the specified RuntimeIdentifier '{0}'. @@ -767,9 +767,9 @@ The following are names of parameters or literal values and should not be transl {StrBegins="NETSDK1189: "} - NETSDK1214: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. - NETSDK1214: PreferNativeArm64 僅適用於 .NET Framework 目標。其不受支援,且在以 .NET Core 為目標時沒有作用。 - {StrBegins="NETSDK1214: "} + NETSDK1222: PreferNativeArm64 applies only to .NET Framework targets. It is not supported and has no effect for when targeting .NET Core. + NETSDK1222: PreferNativeArm64 僅適用於 .NET Framework 目標。其不受支援,且在以 .NET Core 為目標時沒有作用。 + {StrBegins="NETSDK1222: "} NETSDK1011: Assets are consumed from project '{0}', but no corresponding MSBuild project path was found in '{1}'. diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs index 847efc3165dd..248a1470b7b7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs @@ -35,7 +35,9 @@ public class GivenThatWeHaveErrorCodes 1182, 1183, 1190, - 1192 + 1192, + 1213, + 1214 }; //ILLink lives in other repos and violated the _info requirement for no error code diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.targets index eb16ada800c1..6c54a6af19a1 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.targets @@ -129,10 +129,10 @@ Copyright (c) .NET Foundation. All rights reserved. FormatArguments="$(SdkResolverGlobalJsonPath)" /> - - + Condition="$([MSBuild]::VersionLessThan($(MSBuildVersion), '17.12.0')) and '$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals($(_TargetFrameworkVersionWithoutV), '9.0'))"> + - - + + + 11.0 - 17.12 + 17.16 diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf index 56dc331335f9..0397e58fd693 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + Stav nasazení je {0}. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Stav nasazení je {0}: {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Adresa URL stavu nasazení {0} chybí nebo je neplatná. {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Cyklické dotazování na stav nasazení... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Pokus OneDeploy o publikování souboru prostřednictvím {0} se nezdařil. Stavový kód HTTP: {1}. {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Pokus OneDeploy o publikování souboru prostřednictvím {0} se nezdařil. Stavový kód HTTP {1}: {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Nepovedlo se načíst přihlašovací údaje pro publikování. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Pokus OneDeploy o publikování souboru {0} prostřednictvím {1} se nezdařil. Stavový kód: {2}. Projděte si protokoly na {3}. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Soubor pro publikování {0} nebyl nalezen nebo není přístupný. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl {0} chybí nebo má neplatnou hodnotu. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + {0} se publikuje do umístění {1}... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + Nasazení OneDeploy bylo úspěšné. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Soubor {0} byl nahrán do cílové instance. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf index c4413d928128..207e2a510830 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + Der Bereitstellungsstatus ist „{0}“. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Bereitstellungsstatus: „{0}“: {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Die Bereitstellungsstatus-URL „{0}“ fehlt oder ist ungültig. {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Der Bereitstellungsstatus wird abgerufen... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy-Versuch, die Datei über „{0}“ zu veröffentlichen, ist fehlgeschlagen. HTTP-Statuscode: „{1}“. {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy-Versuch, die Datei über „{0}“ zu veröffentlichen, ist fehlgeschlagen. HTTP-Statuscode: „{1}“: {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Fehler beim Abrufen der Anmeldeinformationen für die Veröffentlichung. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Fehler beim OneDeploy-Versuch, die Datei „{0}“ bis „{1}“ zu veröffentlichen. Statuscode: „{2}“. Sehen Sie sich die Protokolle unter „{3}“ an. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Die zu veröffentlichende Datei „{0}“ wurde nicht gefunden oder ist nicht zugänglich. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl „{0}“ fehlt oder weist einen ungültigen Wert auf. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + „{0}“ wird in „{1}“ veröffentlicht... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + OneDeploy-Bereitstellung erfolgreich. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Die Datei „{0}“ wurde in die Zielinstanz hochgeladen. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf index ae528678ccea..02cfc569ec6d 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf @@ -226,22 +226,22 @@ Vínculo redireccionable: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + El estado de implementación es "{0}". {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + El estado de implementación es "{0}": {1}. {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Falta la dirección URL de estado de implementación "{0}" o no es válida {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Sondeando el estado de implementación... @@ -356,47 +356,47 @@ Vínculo redireccionable: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Error en el intento de OneDeploy de publicar el archivo a través de "{0}" con el código de estado HTTP "{1}". {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Error en el intento de OneDeploy de publicar el archivo a través de "{0}" con el código de estado HTTP "{1}": {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + No se pudieron recuperar las credenciales de publicación. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Error en el intento de OneDeploy de publicar el archivo "{0}" a "{1}" con el código de estado "{2}". Vea los registros en "{3}". {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + No se encontró el archivo para publicar "{0}" o no se puede obtener acceso a él. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + Falta PublishUrl "{0}" o tiene un valor no válido. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Publicando "{0}" en "{1}"... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + Implementación de OneDeploy correcta. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Archivo "{0}" cargado en la instancia de destino. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf index fea3cb7722af..009d934fc24a 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf @@ -226,22 +226,22 @@ FWLink : http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + État du déploiement : « {0} ». {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + État du déploiement : « {0} » : {1}. {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Le « {0} » de l’URL de statut de déploiement est manquant ou non valide {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Interrogation de l'état du déploiement... @@ -356,47 +356,47 @@ FWLink : http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + La tentative de OneDeploy de publier un fichier via « {0} » a échoué avec le code d’état HTTP « {1} ». {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + La tentative de OneDeploy de publier un fichier via « {0} » a échoué avec le code d’état HTTP « {1} » : {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Échec de la récupération des informations d’identification de publication. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + La tentative de OneDeploy de publier le fichier « {0} » par « {1} » a échoué avec le code de statut « {2} ». Consultez les journaux sur « {3} ». {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Le fichier à publier « {0} » est introuvable ou n’est pas accessible. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + Le « {0} » PublishUrl est manquant ou a une valeur non valide. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Publication de « {0} » sur « {1} »... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + Déploiement de OneDeploy réussi. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Le fichier « {0} » chargé vers le instance cible. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf index c31079265702..d8e737cd9a8f 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + Lo stato della distribuzione è {0}. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Lo stato della distribuzione è '{0}': {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + L'URL dello stato della distribuzione '{0}' è mancante o non valido {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Polling dello stato della distribuzione... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Il tentativo OneDeploy di pubblicazione del file tramite '{0}' non è riuscito con il codice di stato HTTP: '{1}'. {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Il tentativo OneDeploy di pubblicazione del file tramite '{0}' non è riuscito con il codice di stato HTTP '{1}': {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Non è stato possibile recuperare le credenziali di pubblicazione. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Il tentativo OneDeploy di pubblicazione del file '{0}' tramite '{1}' non è riuscito con il codice di stato '{2}'. Vedere i log in '{3}'. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Il file da pubblicare '{0}' non è stato trovato o non è accessibile. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + Il '{0}' PublishUrl è mancante o ha un valore non valido. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Pubblicazione di '{0}' in '{1}'... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + La distribuzione di OneDeploy è riuscita. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + File '{0}' caricato nell'istanza di destinazione. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf index 5dca3cb12919..9fc69730136c 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + デプロイ状態は '{0}' です。 {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + デプロイ状態は '{0}': {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + デプロイ状態 URL '{0}' が見つからないか無効です {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + 配置状態をポーリングしています... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy が '{0}' を介してファイルを発行しようとしましたが失敗しました、HTTP 状態コード '{1}'。 {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy が '{0}' を介してファイルを発行しようとしましたが失敗しました、HTTP 状態コード '{1}': {2}。 {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + 発行資格情報を取得できませんでした。 OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy でファイル '{1}' から '{0}' を発行しようとしましたが、状態コード '{2}' で失敗しました。'{3}' のログを参照してください。 {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + '{0}' を発行するファイルが見つからないか、アクセスできません。 {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' がないか、無効な値が含まれています。 {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + '{0}' を {1} に発行しています... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + OneDeploy のデプロイに成功しました。 File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + ファイル '{0}' はターゲット インスタンスにアップロードされました。 {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf index ce92d6066e47..58dc4f44b270 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + 배포 상태는 '{0}'입니다. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + 배포 상태는 '{0}'({1})입니다. {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + '{0}' 배포 상태 URL이 없거나 잘못되었습니다. {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + 배포 상태에 대한 폴링... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + '{0}'을(를) 통해 파일을 게시하려는 OneDeploy 시도가 실패했습니다('{1}' HTTP 상태 코드). {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + '{0}'을(를) 통해 파일을 게시하려는 OneDeploy 시도가 실패했습니다('{1}' HTTP 상태 코드: {2}). {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + 게시 자격 증명을 찾아오지 못했습니다. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy에서 '{1}'을(를) 통해 '{0}' 파일을 '(으)로 게시하려는 시도가 실패했습니다(상태 코드: '{2}'). '{3}'의 로그를 참조하세요. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + '{0}'을(를) 게시할 파일을 찾을 수 없거나 액세스할 수 없습니다. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + '{0}' PublishUrl이 없거나 값이 잘못되었습니다. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + '{0}'을(를) '{1}에 게시하는 중... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + OneDeploy를 배포했습니다. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + 대상 인스턴스에 '{0}' 파일을 업로드했습니다. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf index 8218f58524a3..65a2ef1b201a 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + Stan wdrożenia to „{0}”. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Stan wdrożenia to „{0}”: {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Brak adresu URL stanu wdrożenia „{0}” lub jest on nieprawidłowy {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Sondowanie stanu wdrożenia... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Próba opublikowania pliku przez rozwiązanie OneDeploy za pomocą "{0}" nie powiodła się. Kod stanu HTTP"{1}". {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Próba opublikowania pliku przez rozwiązanie OneDeploy za pomocą „{0}” nie powiodła się. Kod stanu HTTP „{1}”: {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Nie można pobrać poświadczenia publikowania. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Próba opublikowania pliku „{0}” za pomocą „{1}” przez rozwiązanie OneDeploy nie powiodło się. Kod stanu: „{2}”. Zobacz dzienniki w lokalizacji „{3}”. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Nie znaleziono pliku do opublikowania „{0}” lub jest on niedostępny. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + Brak elementu PublishUrl „{0}” lub ma on nieprawidłową wartość. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Trwa publikowanie „{0}” do „{1}”... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + Wdrożenie OneDeploy powiodło się. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Plik „{0}” został przekazany do wystąpienia miejsce docelowego. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf index 8a7ffbfd0bf3..e7b7f2f264dd 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + O status da implantação é '{0}'. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + O status da implantação é '{0}': {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + A URL de status da implantação '{0}' está ausente ou é inválida {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Sondando o status da implantação... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + A tentativa do OneDeploy de publicar o arquivo através de '{0}' falhou com o código de status HTTP '{1}'. {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + A tentativa do OneDeploy de publicar o arquivo através de '{0}' falhou com o código de status HTTP '{1}': {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Falha ao recuperar as credenciais de publicação. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + A tentativa do OneDeploy de publicar o arquivo '{0}' através de '{1}' falhou com o código de status '{2}'. Veja os logs em '{3}'. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + O arquivo para publicar '{0}' não foi encontrado ou não está acessível. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + A PublishUrl '{0}' está ausente ou tem um valor inválido. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Publicação de '{0}' para '{1}'... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + A implantação do OneDeploy foi bem-sucedida. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + O arquivo '{0}' foi enviado para a instância de meta. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf index 8c22509aa93f..7ac0ba67693b 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068. Deployment status is '{0}'. - Deployment status is '{0}'. + Состояние развертывания: {0}. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Состояние развертывания: {0}: {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + URL-адрес состояния развертывания '{0}' отсутствует или недопустим {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Опрос состояния развертывания… @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068. OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Попытка OneDeploy опубликовать файл с помощью '{0}' не удалась, код состояния HTTP '{1}': {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Попытка OneDeploy опубликовать файл с помощью '{0}' не удалась, код состояния HTTP '{1}': {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Не удалось получить учетные данные публикации. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + Попытка OneDeploy опубликовать файл с помощью '{0}' не удалась, код состояния '{1}': {2}. Просмотреть журналы в '{3}'. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + Файл для публикации {0} не найден или недоступен. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl {0} отсутствует или имеет недопустимое значение. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + Публикация ZIP-файла {0} в {1}… {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + Развертывание OneDeploy выполнено успешно. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Файл '{0}' отправлен в целевой экземпляр. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf index 0d6c30f5602a..d5e5e7715b62 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + Dağıtım durumu '{0}'. {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + Dağıtım durumu '{0}': {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + Dağıtım durumu URL'si '{0}' eksik veya geçersiz {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + Dağıtım durumu yoklanıyor... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + Dosyayı '{0}' aracılığıyla yayımlamaya yönelik OneDeploy girişimi '{1}' HTTP durum koduyla başarısız oldu. {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + Dosyayı '{0}' aracılığıyla yayımlamaya yönelik OneDeploy girişimi '{1}' HTTP durum koduyla başarısız oldu: {2}. {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + Yayımlama kimlik bilgileri alınamadı. OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + '{0}' dosyasını '{1}' aracılığıyla yayımlamaya yönelik OneDeploy girişimi '{2}' durum koduyla başarısız oldu. '{3}' konumundaki günlüklere bakın. {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + '{0}' öğesini yayımlamak için dosya bulunamadı veya erişilebilir değil. {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' eksik veya geçersiz bir değere sahip. {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + '{0}', '{1}' konumunda yayımlanıyor... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + OneDeploy dağıtımı başarılı oldu. File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + Dosya '{0}' hedef örneğe yüklendi. {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf index 6eb0a8098a06..dd1824cb2ffd 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + 部署状态为 ‘{0}’。 {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + 部署状态为 ‘{0}’: {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + 部署状态 URL ‘{0}’ 缺失或无效 {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + 正在轮询部署状态… @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy 尝试通过 ‘{0}’ 发布文件失败,HTTP 状态代码为 ‘{1}’。 {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy 尝试通过 ‘{0}’ 发布文件失败,HTTP 状态代码 ‘{1}’: {2}。 {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + 无法检索发布凭据。 OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy 尝试通过 ‘{1}’ 发布文件 ‘{0}’ 失败,状态代码为 ‘{2}’。请参阅位于 ‘{3}’ 的日志。 {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + 找不到或无法访问要发布 ‘{0}’ 的文件。 {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + PublishUrl ‘{0}’ 缺失或具有无效值。 {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + 正在将 ‘{0}’ 发布到 {1}... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + OneDeploy 部署成功。 File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + 文件 ‘{0}’ 已上传到目标实例。 {0} - file to publish. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf index 96c6046aa817..a2f48027a55b 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf @@ -226,22 +226,22 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Deployment status is '{0}'. - Deployment status is '{0}'. + 部署狀態為 '{0}'。 {0} - deployment status name Deployment status is '{0}': {1} - Deployment status is '{0}': {1} + 部署狀態為 '{0}': {1} {0} - deployment status name, {1} - deployment status text Deployment status URL '{0}' is missing or invalid - Deployment status URL '{0}' is missing or invalid + 缺少部署狀態 URL '{0}',或其無效 {0} - deployment polling URL Polling for deployment status... - Polling for deployment status... + 正在輪詢部署狀態... @@ -356,47 +356,47 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy 嘗試透過 '{0}' 發佈檔案時失敗,HTTP 狀態代碼為 '{1}'。 {0} - publish URL, {1} - HTTP response status code OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. - OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy 嘗試透過 '{0}' 發佈檔案時失敗,HTTP 狀態代碼 '{1}': {2}。 {0} - publish URL, {1} - HTTP response status code, {2} - response body as text Failed to retrieve publish credentials. - Failed to retrieve publish credentials. + 無法擷取發佈認證。 OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. - OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy 嘗試透過 '{1}' 發佈檔案 '{0}' 時失敗,狀態代碼為 '{2}'。請前往 '{3}' 查看記錄。 {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL File to publish '{0}' was not found or is not accessible. - File to publish '{0}' was not found or is not accessible. + 找不到或無法存取要發佈的檔案 '{0}'。 {0} - file to publish PublishUrl '{0}' is missing or has an invalid value. - PublishUrl '{0}' is missing or has an invalid value. + 缺少 PublishUrl '{0}',或值無效。 {0} - publish URL Publishing '{0}' to '{1}'... - Publishing '{0}' to '{1}'... + 正在將 '{0}' 發佈至 '{1}'... {0} - file to publish, {1} - publish URL OneDeploy deployment succeeded. - OneDeploy deployment succeeded. + 成功部署 OneDeploy。 File '{0}' uploaded to target instance. - File '{0}' uploaded to target instance. + 已將檔案 '{0}' 上傳至目標執行個體。 {0} - file to publish. diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeManifestSupportedFrameworks.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeManifestSupportedFrameworks.cs index be9ba6ab5988..4445fcaa465f 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeManifestSupportedFrameworks.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeManifestSupportedFrameworks.cs @@ -9,7 +9,7 @@ public GivenThatWeManifestSupportedFrameworks(ITestOutputHelper log) : base(log) { } - [RequiresMSBuildVersionTheory("17.8.0")] + [RequiresMSBuildVersionTheory("17.12.0")] [InlineData(".NETCoreApp")] [InlineData(".NETStandard")] public void TheMaximumVersionsAreSupported(string targetFrameworkIdentifier) diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs index 31167bf1de5e..0a701c97c8cd 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs @@ -472,7 +472,7 @@ private void AssertDefinedConstantsOutput(TestAsset testAsset, string targetFram definedConstants.Should().BeEquivalentTo(new[] { "DEBUG", "TRACE" }.Concat(expectedDefines).ToArray()); } - [WindowsOnlyTheory] + [WindowsOnlyRequiresMSBuildVersionTheory("17.12.0")] [InlineData("net8.0", new[] { "NETCOREAPP", "NET", "NET8_0", "NET8_0_OR_GREATER" })] [InlineData("net9.0", new[] { "NETCOREAPP", "NET", "NET8_0_OR_GREATER", "NET9_0_OR_GREATER", "NET9_0", "WINDOWS", "WINDOWS7_0", "WINDOWS7_0_OR_GREATER" }, "windows", "7.0")] public void It_can_use_implicitly_defined_compilation_constants(string targetFramework, string[] expectedOutput, string targetPlatformIdentifier = null, string targetPlatformVersion = null) diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs index fafc1185848a..f09c80a99314 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToFloatWarningLevels.cs @@ -375,7 +375,7 @@ static void Main() [InlineData("recommended", "true", new string[] { "CA1310", "CA1068", "CA2200" })] [InlineData("all", "false", new string[] { "CA1031", "CA1310", "CA1068", "CA2200" })] [InlineData("all", "true", new string[] { "CA1031", "CA1310", "CA1068", "CA2200" })] - [RequiresMSBuildVersionTheory("17.8.0")] + [RequiresMSBuildVersionTheory("17.12.0")] public void It_bulk_configures_rules_with_different_analysis_modes(string analysisMode, string codeAnalysisTreatWarningsAsErrors, string[] expectedViolations) { var testProject = new TestProject diff --git a/test/Microsoft.NET.Build.Tests/SourceLinkTests.cs b/test/Microsoft.NET.Build.Tests/SourceLinkTests.cs index 109a9cfc2c9e..ed364c84e408 100644 --- a/test/Microsoft.NET.Build.Tests/SourceLinkTests.cs +++ b/test/Microsoft.NET.Build.Tests/SourceLinkTests.cs @@ -112,7 +112,7 @@ public void WithNoGitMetadata() /// /// When creating a new repository locally we want the build to work and not report warnings even before the remote is set. /// - [RequiresMSBuildVersionFact("17.8.0")] + [RequiresMSBuildVersionFact("17.12.0")] public void WithNoRemoteNoCommit() { var testAsset = _testAssetsManager @@ -131,7 +131,7 @@ public void WithNoRemoteNoCommit() /// /// When creating a new repository locally we want the build to work and not report warnings even before the remote is set. /// - [RequiresMSBuildVersionFact("17.8.0")] + [RequiresMSBuildVersionFact("17.12.0")] public void WithNoRemote() { var testAsset = _testAssetsManager diff --git a/test/Microsoft.NET.Clean.Tests/GivenThatWeWantToCleanAProject.cs b/test/Microsoft.NET.Clean.Tests/GivenThatWeWantToCleanAProject.cs index c00d909febcd..2e50ad96a554 100644 --- a/test/Microsoft.NET.Clean.Tests/GivenThatWeWantToCleanAProject.cs +++ b/test/Microsoft.NET.Clean.Tests/GivenThatWeWantToCleanAProject.cs @@ -12,7 +12,7 @@ public GivenThatWeWantToCleanAHelloWorldProject(ITestOutputHelper log) : base(lo { } - [RequiresMSBuildVersionFact("17.8.0")] + [RequiresMSBuildVersionFact("17.12.0")] public void It_cleans_without_logging_assets_message() { var testAsset = _testAssetsManager diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs index 891b538f2478..44679e8ded2e 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs @@ -19,7 +19,7 @@ public GivenThatWeWantToPublishAnAotApp(ITestOutputHelper log) : base(log) { } - [RequiresMSBuildVersionTheory("17.8.0")] + [RequiresMSBuildVersionTheory("17.12.0")] [MemberData(nameof(Net7Plus), MemberType = typeof(PublishTestUtils))] public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_enabled(string targetFramework) { @@ -91,7 +91,7 @@ public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_enabled(string .And.HaveStdOutContaining("Hello World"); } - [RequiresMSBuildVersionTheory("17.8.0")] + [RequiresMSBuildVersionTheory("17.12.0")] [MemberData(nameof(Net7Plus), MemberType = typeof(PublishTestUtils))] public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_false(string targetFramework) { diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishReadyToRun.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishReadyToRun.cs index 06259ae936d6..e17ce09acbff 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishReadyToRun.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishReadyToRun.cs @@ -45,8 +45,6 @@ public void It_only_runs_readytorun_compiler_when_switch_is_enabled(string targe } [RequiresMSBuildVersionTheory("17.0.0.32901")] - [InlineData("netcoreapp3.0")] - [InlineData("net5.0")] [InlineData(ToolsetInfo.CurrentTargetFramework)] public void It_creates_readytorun_images_for_all_assemblies_except_excluded_ones(string targetFramework) { @@ -91,8 +89,6 @@ public void It_creates_readytorun_images_for_all_assemblies_except_excluded_ones } [RequiresMSBuildVersionTheory("17.0.0.32901")] - [InlineData("netcoreapp3.0")] - [InlineData("net5.0")] [InlineData(ToolsetInfo.CurrentTargetFramework)] public void It_creates_readytorun_symbols_when_switch_is_used(string targetFramework) { @@ -100,8 +96,6 @@ public void It_creates_readytorun_symbols_when_switch_is_used(string targetFrame } [RequiresMSBuildVersionTheory("17.0.0.32901")] - [InlineData("netcoreapp3.0")] - [InlineData("net5.0")] [InlineData(ToolsetInfo.CurrentTargetFramework)] public void It_supports_framework_dependent_publishing(string targetFramework) { @@ -187,8 +181,6 @@ public void It_warns_when_targetting_netcoreapp_2_x_readytorun() } [RequiresMSBuildVersionTheory("17.0.0.32901")] - [InlineData("netcoreapp3.0")] - [InlineData("net5.0")] [InlineData(ToolsetInfo.CurrentTargetFramework)] public void It_can_publish_readytorun_for_library_projects(string targetFramework) { @@ -196,8 +188,6 @@ public void It_can_publish_readytorun_for_library_projects(string targetFramewor } [RequiresMSBuildVersionTheory("17.0.0.32901")] - [InlineData("netcoreapp3.0")] - [InlineData("net5.0")] [InlineData(ToolsetInfo.CurrentTargetFramework)] public void It_can_publish_readytorun_for_selfcontained_library_projects(string targetFramework) { diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs index 03e3171e3c00..d77c46042580 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -1662,49 +1662,6 @@ public void ILLink_dont_display_time_awareness_message_on_incremental_build(stri .Should().Pass().And.NotHaveStdErrContaining("This process might take a while"); } - [Fact()] - public void ILLink_and_crossgen_process_razor_assembly() - { - var targetFramework = "netcoreapp3.0"; - var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); - - var testProject = new TestProject - { - Name = "TestWeb", - IsExe = true, - ProjectSdk = "Microsoft.NET.Sdk.Web", - TargetFrameworks = targetFramework, - SourceFiles = - { - ["Program.cs"] = @" - class Program - { - static void Main() {} - }", - ["Test.cshtml"] = @" - @page - @{ - System.IO.Compression.ZipFile.OpenRead(""test.zip""); - } - ", - }, - AdditionalProperties = - { - ["RuntimeIdentifier"] = rid, - ["PublishTrimmed"] = "true", - ["PublishReadyToRun"] = "true", - } - }; - - var testAsset = _testAssetsManager.CreateTestProject(testProject); - var publishCommand = new PublishCommand(testAsset); - publishCommand.Execute().Should().Pass(); - - var publishDir = publishCommand.GetOutputDirectory(targetFramework, runtimeIdentifier: rid); - publishDir.Should().HaveFile("System.IO.Compression.ZipFile.dll"); - GivenThatWeWantToPublishReadyToRun.DoesImageHaveR2RInfo(publishDir.File("TestWeb.Views.dll").FullName); - } - [RequiresMSBuildVersionTheory("17.0.0.32901")] [InlineData(ToolsetInfo.CurrentTargetFramework, true)] [InlineData(ToolsetInfo.CurrentTargetFramework, false)] diff --git a/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs b/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs index 7307d22114de..0ac32c34ca6e 100644 --- a/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs +++ b/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs @@ -9,7 +9,8 @@ internal static class PublishTestUtils public static IEnumerable SupportedTfms { get; } = new List { - new object[] { "netcoreapp3.1" }, + // Some tests started failing on net3.1 so disabling since this has been out of support for a while + //new object[] { "netcoreapp3.1" }, new object[] { "net5.0" }, new object[] { "net6.0" }, new object[] { "net7.0" }, diff --git a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj index 7ef173306cc0..181ea7cfd6b5 100644 --- a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj +++ b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj @@ -33,6 +33,10 @@ <_Parameter1>MicrosoftAspNetCoreAppRefPackageVersion <_Parameter2>$(MicrosoftAspNetCoreAppRefPackageVersion) + + <_Parameter1>MicrosoftNETSdkAspireManifest80100PackageVersion + <_Parameter2>$(MicrosoftNETSdkAspireManifest80100PackageVersion) + diff --git a/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs b/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs index 69d06b8c0479..30ddc62054a3 100644 --- a/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs +++ b/test/Microsoft.WebTools.AspireService.Tests/AspireServerServiceTests.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using System.Net.WebSockets; @@ -13,12 +14,11 @@ namespace Microsoft.WebTools.AspireServer.UnitTests; -public class AspireServerServiceTests +public class AspireServerServiceTests(ITestOutputHelper output) { private const string Project1Path = @"c:\test\Projects\project1.csproj"; private const int ProcessId = 34213; private const string DcpId = "myid"; - private const string SpecificProfileName = "SpecificProfile"; private const string VersionedSessionUrl = $"{RunSessionRequest.Url}?{RunSessionRequest.VersionQuery}={RunSessionRequest.OurProtocolVersion}"; private static readonly TestRunSessionRequest Project1SessionRequest = new TestRunSessionRequest(Project1Path, debugging: false, launchProfile: null, disableLaunchProfile: false) @@ -37,15 +37,15 @@ public async Task SessionStarted_Test() // Start listening TaskCompletionSource connected = new(); - TaskCompletionSource notificationTask = new(); - _ = listenForSessionUpdatesAsync(server, connected, (sn) => + TaskCompletionSource notificationTask = new(); + _ = ListenForSessionUpdatesAsync(server, connected, (sn) => { - notificationTask.SetResult((SessionChangeNotification)sn); + notificationTask.SetResult((ProcessRestartedNotification)sn); }); await connected.Task; - await server.SessionStartedAsync(DcpId,"1", ProcessId, CancellationToken.None); + await server.NotifySessionStartedAsync(DcpId,"1", ProcessId, CancellationToken.None); var result = await notificationTask.Task; @@ -66,21 +66,21 @@ public async Task SessionEndedAsync_Test() // Start listening TaskCompletionSource connected = new(); - TaskCompletionSource sessionEndNotificationTask = new(); - _ = listenForSessionUpdatesAsync(server, connected, (sn) => + TaskCompletionSource sessionEndNotificationTask = new(); + _ = ListenForSessionUpdatesAsync(server, connected, (sn) => { if (sn.NotificationType == NotificationType.SessionTerminated) { - sessionEndNotificationTask.SetResult((SessionChangeNotification)sn); + sessionEndNotificationTask.SetResult((SessionTerminatedNotification)sn); } }); await connected.Task; - await server.SessionEndedAsync(DcpId, "1", ProcessId, 130, CancellationToken.None); + await server.NotifySessionEndedAsync(DcpId, "1", ProcessId, 130, CancellationToken.None); var result = await sessionEndNotificationTask.Task; - Assert.Equal(ProcessId, result.PID); + Assert.Equal(ProcessId, result.Pid); Assert.Equal("1", result.SessionId); Assert.Equal(130, result.ExitCode); @@ -98,7 +98,7 @@ public async Task LaunchProject_Success() .ImplementStartProjectAsync(DcpId, "2"); var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); @@ -124,7 +124,7 @@ public async Task LaunchProject_Success_ThenStopProcessRequest() .ImplementStopSessionAsync(DcpId, "3", exists: false); var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); @@ -154,7 +154,7 @@ public async Task LaunchProject_FailedToLaunchProject() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); var response = await client.PutAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); @@ -174,7 +174,7 @@ public async Task LaunchProject_FailNoBearerToken() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); @@ -193,7 +193,7 @@ public async Task LaunchProject_FailWrongUrl() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); var response = await client.PutAsJsonAsync("/run_badurl", Project1SessionRequest); @@ -212,7 +212,7 @@ public async Task LaunchProject_NotAPUTRequest() var aspireServer = await GetAspireServer(mocks); - var tokens = await aspireServer.GetServerVariablesAsync(); + var tokens = aspireServer.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); var response = await client.PostAsJsonAsync(VersionedSessionUrl, Project1SessionRequest); @@ -231,7 +231,7 @@ public async Task StopSession_FailNoBearerToken() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); @@ -250,7 +250,7 @@ public async Task Info_Success() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); var response = await client.GetAsync(InfoResponse.Url); @@ -268,7 +268,7 @@ public async Task Info_FailNoBearerToken() var server = await GetAspireServer(mocks); - var tokens = await server.GetServerVariablesAsync(); + var tokens = server.GetServerVariables(); using HttpClient client = GetHttpClient(tokens); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "badToken"); @@ -290,15 +290,15 @@ public async Task SendLogMessageAsync_Test() // Start listening TaskCompletionSource connected = new(); - TaskCompletionSource notificationTask = new(); - _ = listenForSessionUpdatesAsync(aspireServer, connected, (sn) => + TaskCompletionSource notificationTask = new(); + _ = ListenForSessionUpdatesAsync(aspireServer, connected, (sn) => { - notificationTask.SetResult((SessionLogsNotification)sn); + notificationTask.SetResult((ServiceLogsNotification)sn); }); await connected.Task; - await aspireServer.SendLogMessageAsync(DcpId, "1", isStdErr: false, "My Message", CancellationToken.None); + await aspireServer.NotifyLogMessageAsync(DcpId, "1", isStdErr: false, "My Message", CancellationToken.None); var result = await notificationTask.Task; @@ -317,29 +317,29 @@ public async Task GetEnvironmentForOrchestrator_Tests() var server = await GetAspireServer(mocks, waitForListening: false); // First time should create a key - var envVars = await server.GetServerConnectionEnvironmentAsync(CancellationToken.None); + var envVars = server.GetServerConnectionEnvironment(); Assert.Equal(3, envVars.Count); var token = envVars[1]; Assert.NotNull(token.Value); // Should return the same - envVars = await server.GetServerConnectionEnvironmentAsync(CancellationToken.None); + envVars = server.GetServerConnectionEnvironment(); Assert.Equal(token, envVars[1]); mocks.Verify(); } - private async Task listenForSessionUpdatesAsync(AspireServerService aspireServer, TaskCompletionSource connected, Action callback) + private async Task ListenForSessionUpdatesAsync(AspireServerService aspireServer, TaskCompletionSource connected, Action callback) { - var tokens = await aspireServer.GetServerVariablesAsync(); + var tokens = aspireServer.GetServerVariables(); using var httpClient = GetHttpClient(tokens); using var ws = new ClientWebSocket(); ws.Options.SetRequestHeader("Authorization", $"Bearer {tokens.bearerToken}"); try { - await ws.ConnectAsync(new Uri($"wss://{tokens.serverAddress}{RunSessionRequest.Url}{SessionNotificationBase.Url}"), httpClient, CancellationToken.None); + await ws.ConnectAsync(new Uri($"wss://{tokens.serverAddress}{RunSessionRequest.Url}{SessionNotification.Url}"), httpClient, CancellationToken.None); } catch (Exception ex) { @@ -352,53 +352,37 @@ private async Task listenForSessionUpdatesAsync(AspireServerService aspireServer while (ws.State == WebSocketState.Open) { + string message; try { - var (message, messageType) = await GetSocketMsgAsync(ws); + (message, var messageType) = await GetSocketMsgAsync(ws); if (messageType == WebSocketMessageType.Close) { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); return; } - else - { - var notificationBase = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); - if (notificationBase is null) - { - Console.WriteLine("Unexpected null SessionNotificationBase message"); - } - else if (notificationBase.NotificationType == NotificationType.ProcessRestarted || notificationBase.NotificationType == NotificationType.SessionTerminated) - { - var scn = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); - if (scn is null) - { - Assert.Fail("Unexpected null SessionChangeNotification message"); - } - else - { - callback.Invoke(scn); - } - } - else if (notificationBase.NotificationType == NotificationType.ServiceLogs) - { - var sessionLogs = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); - if (sessionLogs is null) - { - Assert.Fail("Unexpected null SessionLogsNotification message"); - } - else - { - callback.Invoke(sessionLogs); - } - } - } } catch { // This is expected if the connection is closed + Assert.Equal(WebSocketState.Closed, ws.State); return; } + + var notification = JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions); + Assert.NotNull(notification); + + SessionNotification value = notification.NotificationType switch + { + NotificationType.ProcessRestarted => JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions), + NotificationType.SessionTerminated => JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions), + NotificationType.ServiceLogs => JsonSerializer.Deserialize(message, AspireServerService.JsonSerializerOptions), + _ => throw new InvalidOperationException($"Unexpected {notification.NotificationType}") + }; + + Assert.NotNull(value); + callback.Invoke(value); } } @@ -440,9 +424,14 @@ private static HttpClient GetHttpClient((string serverAddress, string bearerToke private async Task GetAspireServer(Mocks mocks, bool waitForListening = true) { - var ase = mocks.GetOrCreate(); + var serverEvents = mocks.GetOrCreate(); - var aspireServer = new AspireServerService(ase.Object, displayName: "Test server", Console.WriteLine); + var aspireServer = new AspireServerService(serverEvents.Object, displayName: "Test server", + line => + { + output.WriteLine(line); + Debug.WriteLine(line); + }); if (waitForListening) { @@ -517,16 +506,16 @@ internal static class AspireServerServiceExtensions { public static async Task WaitForListeningAsync(this AspireServerService aspireServer) { - string serverAddress = (await aspireServer.GetServerVariablesAsync()).serverAddress; + string serverAddress = aspireServer.GetServerVariables().serverAddress; // We need to wait on the port being available await Helpers.CanConnectToPortAsync(new Uri($"http://{serverAddress}"), 5000, CancellationToken.None); } - public static async Task<(string serverAddress, string bearerToken, string certToken)> GetServerVariablesAsync(this AspireServerService aspireServer) + public static (string serverAddress, string bearerToken, string certToken) GetServerVariables(this AspireServerService aspireServer) { - var enVars = await aspireServer.GetServerConnectionEnvironmentAsync(CancellationToken.None); + var enVars = aspireServer.GetServerConnectionEnvironment(); return (enVars[0].Value, enVars[1].Value, enVars[2].Value); } } diff --git a/test/TestAssets/TestProjects/ProjectReferences_Graph/E/E.csproj b/test/TestAssets/TestProjects/ProjectReferences_Graph/E/E.csproj index e068485fb8fb..0646f5921788 100644 --- a/test/TestAssets/TestProjects/ProjectReferences_Graph/E/E.csproj +++ b/test/TestAssets/TestProjects/ProjectReferences_Graph/E/E.csproj @@ -1,5 +1,5 @@ - + - netstandard2.1 + netstandard2.0 diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs new file mode 100644 index 000000000000..d846ba189770 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Program.cs @@ -0,0 +1,39 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults & Aspire components. +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.AddProblemDetails(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseExceptionHandler(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}); + +app.MapDefaultEndpoints(); + +app.Run(); + +internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Properties/launchSettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Properties/launchSettings.json new file mode 100644 index 000000000000..4139a6bdb05b --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "http://localhost:5303", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/WatchAspire.ApiService.csproj b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/WatchAspire.ApiService.csproj new file mode 100644 index 000000000000..22dc3f3b39a2 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/WatchAspire.ApiService.csproj @@ -0,0 +1,14 @@ + + + + Exe + $(CurrentTargetFramework) + enable + enable + + + + + + + diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.Development.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs new file mode 100644 index 000000000000..f56c3b8319aa --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Program.cs @@ -0,0 +1,5 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("apiservice"); + +builder.Build().Run(); diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Properties/launchSettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000000..b72d77798ccb --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17211;http://localhost:15137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21185", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22024" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15137", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19235", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20033", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj new file mode 100644 index 000000000000..bb5e72a5337b --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/WatchAspire.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + $(CurrentTargetFramework) + enable + enable + true + ad800ccc-954c-40cc-920b-2e09fc9eee7a + + 9.0.0 + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.Development.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.json b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.json new file mode 100644 index 000000000000..31c092aa4501 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/Extensions.cs b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000000..332cb237f92a --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/Extensions.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // app.MapPrometheusScrapingEndpoint(); + } + + return app; + } +} diff --git a/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/WatchAspire.ServiceDefaults.csproj b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/WatchAspire.ServiceDefaults.csproj new file mode 100644 index 000000000000..cbfdf8929cb2 --- /dev/null +++ b/test/TestAssets/TestProjects/WatchAspire/WatchAspire.ServiceDefaults/WatchAspire.ServiceDefaults.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + true + + + + + + + + diff --git a/test/dotnet-watch.Tests/ConsoleReporterTests.cs b/test/dotnet-watch.Tests/ConsoleReporterTests.cs index 2c3d74db2ab1..b7c4065cc4e6 100644 --- a/test/dotnet-watch.Tests/ConsoleReporterTests.cs +++ b/test/dotnet-watch.Tests/ConsoleReporterTests.cs @@ -17,8 +17,8 @@ public void WritesToStandardStreams(bool suppressEmojis) var dotnetWatchDefaultPrefix = $"dotnet watch {(suppressEmojis ? ":" : "⌚")} "; // stdout - reporter.Verbose("verbose"); - Assert.Equal($"{dotnetWatchDefaultPrefix}verbose" + EOL, testConsole.GetOutput()); + reporter.Verbose("verbose {0}"); + Assert.Equal($"{dotnetWatchDefaultPrefix}verbose {{0}}" + EOL, testConsole.GetOutput()); testConsole.Clear(); reporter.Output("out"); diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index 0a138e01a9f6..b01fc4073e17 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.RegularExpressions; using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Tools.Internal; @@ -191,19 +192,19 @@ public async Task BlazorWasm() App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); - await App.AssertOutputLineStartsWith(MessageDescriptor.ConfiguredToUseBrowserRefresh); - await App.AssertOutputLineStartsWith(MessageDescriptor.ConfiguredToLaunchBrowser); - await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: http://localhost:5000/"); await App.AssertWaitingForChanges(); - // TODO: enable once https://github.com/dotnet/razor/issues/10818 is fixed - //var newSource = """ - // @page "/" - //

Updated

- // """; + App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + App.AssertOutputContains("dotnet watch ⌚ Launching browser: http://localhost:5000/"); - //UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource); - //await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded); + var newSource = """ + @page "/" +

Updated

+ """; + + UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource); + await App.AssertOutputLineStartsWith(MessageDescriptor.HotReloadSucceeded, "blazorwasm (net9.0)"); } [Fact] @@ -370,5 +371,44 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath await App.AssertOutputLineStartsWith("> NewSubdir"); } + + [Fact(Skip = "https://github.com/dotnet/sdk/issues/42850")] + public async Task Aspire() + { + var testAsset = TestAssets.CopyTestAsset("WatchAspire") + .WithSource(); + + var workloadInstallCommandSpec = new DotnetCommand(Logger, ["workload", "install", "aspire", "--include-previews"]) + { + WorkingDirectory = testAsset.Path, + }; + + var result = workloadInstallCommandSpec.Execute(); + Assert.Equal(0, result.ExitCode); + + var serviceSourcePath = Path.Combine(testAsset.Path, "WatchAspire.ApiService", "Program.cs"); + App.Start(testAsset, ["-lp", "http"], relativeProjectDirectory: "WatchAspire.AppHost"); + + await App.AssertWaitingForChanges(); + + // check that Aspire server output is logged via dotnet-watch reporter: + await App.WaitUntilOutputContains("dotnet watch ⭐ Now listening on:"); + + // wait until after DCP session started: + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #1"); + + var newSource = File.ReadAllText(serviceSourcePath, Encoding.UTF8); + newSource = newSource.Replace("Enumerable.Range(1, 5)", "Enumerable.Range(1, 10)"); + UpdateSourceFile(serviceSourcePath, newSource); + + await App.AssertOutputLineStartsWith("dotnet watch 🔥 Hot reload change handled"); + + App.AssertOutputContains("Using Aspire process launcher."); + App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, "WatchAspire.AppHost (net10.0)"); + App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, "WatchAspire.ApiService (net10.0)"); + + // 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: "))); + } } } diff --git a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs index 3187af62bf10..7c6bf4f191bd 100644 --- a/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs +++ b/test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Tools.Internal; @@ -21,7 +23,14 @@ public async Task ReferenceOutputAssembly_False() var reporter = new TestReporter(Logger); var options = TestOptions.GetProjectOptions(["--project", hostProject]); - var projectGraph = Program.TryReadProject(options, reporter); + var factory = new MSBuildFileSetFactory( + rootProjectFile: options.ProjectPath, + targetFramework: null, + buildProperties: [], + environmentOptions: new EnvironmentOptions(Environment.CurrentDirectory, "dotnet"), + reporter); + + var projectGraph = factory.TryLoadProjectGraph(projectGraphRequired: false); var handler = new CompilationHandler(reporter); await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None); diff --git a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs index f56d7bcc42eb..d0c23f109cf3 100644 --- a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs +++ b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs @@ -3,6 +3,7 @@ #nullable enable +using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tests; @@ -16,6 +17,43 @@ public enum TriggerEvent WaitingForChanges, } + private static async Task Launch(string projectPath, TestRuntimeProcessLauncher service, string workingDirectory, CancellationToken cancellationToken) + { + var projectOptions = new ProjectOptions() + { + IsRootProject = false, + ProjectPath = projectPath, + WorkingDirectory = workingDirectory, + BuildProperties = [], + Command = "run", + CommandArguments = ["--project", projectPath], + LaunchEnvironmentVariables = [], + LaunchProfileName = null, + NoLaunchProfile = true, + TargetFramework = null, + }; + + RestartOperation? startOp = null; + startOp = new RestartOperation(async (build, cancellationToken) => + { + var result = await service.ProjectLauncher.TryLaunchProcessAsync( + projectOptions, + new CancellationTokenSource(), + onOutput: null, + restartOperation: startOp!, + build, + cancellationToken); + + Assert.NotNull(result); + + await result.WaitForProcessRunningAsync(cancellationToken); + + return result; + }); + + return await startOp(build: false, cancellationToken); + } + [Theory(Skip="https://github.com/dotnet/sdk/issues/42850")] [CombinatorialData] public async Task UpdateAndRudeEdit(TriggerEvent trigger) @@ -77,8 +115,14 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger) return; } - Launch(serviceProjectA, launchCompletionA).Wait(); - Launch(serviceProjectB, launchCompletionB).Wait(); + // service should have been created before Hot Reload session started: + Assert.NotNull(service); + + Launch(serviceProjectA, service, workingDirectory, watchCancellationSource.Token).Wait(); + launchCompletionA.TrySetResult(); + + Launch(serviceProjectB, service, workingDirectory, watchCancellationSource.Token).Wait(); + launchCompletionB.TrySetResult(); }); var waitingForChanges = reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); @@ -118,42 +162,14 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger) Assert.Equal(4, launchedProcessCount); - async Task Launch(string projectPath, TaskCompletionSource completion) - { - // service should have been created before Hot Reload session started: - Assert.NotNull(service); - - var processTerminationSource = new CancellationTokenSource(); - var projectOptions = new ProjectOptions() - { - IsRootProject = false, - ProjectPath = projectPath, - WorkingDirectory = workingDirectory, - BuildProperties = [], - Command = "run", - CommandArguments = ["--project", projectPath], - LaunchEnvironmentVariables = [], - LaunchProfileName = null, - NoLaunchProfile = true, - TargetFramework = null, - }; - - var runningProject = await service.ProjectLauncher.TryLaunchProcessAsync(projectOptions, processTerminationSource, build: false, watchCancellationSource.Token); - Assert.NotNull(runningProject); - - await runningProject.WaitForProcessRunningAsync(CancellationToken.None); - - completion.SetResult(); - } - // Hot Reload shared dependency - should update both service projects async Task MakeValidDependencyChange() { var hasUpdateSourceA = new TaskCompletionSource(); var hasUpdateSourceB = new TaskCompletionSource(); - reporter.OnProcessOutput += (projectPath, output) => + reporter.OnProjectProcessOutput += (projectPath, line) => { - if (output.Contains("")) + if (line.Content.Contains("")) { if (projectPath == serviceProjectA) { @@ -202,9 +218,9 @@ public static void Common() async Task MakeRudeEditChange() { var hasUpdateSource = new TaskCompletionSource(); - reporter.OnProcessOutput += (projectPath, output) => + reporter.OnProjectProcessOutput += (projectPath, line) => { - if (projectPath == serviceProjectA && output.Contains("Started A: 2")) + if (projectPath == serviceProjectA && line.Content.Contains("Started A: 2")) { hasUpdateSource.SetResult(); } @@ -278,9 +294,9 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput) var hasUpdateA = new SemaphoreSlim(initialCount: 0); var hasUpdateB = new SemaphoreSlim(initialCount: 0); - reporter.OnProcessOutput += (projectPath, output) => + reporter.OnProjectProcessOutput += (projectPath, line) => { - if (output.Contains("")) + if (line.Content.Contains("")) { if (projectPath == serviceProjectA) { @@ -302,7 +318,10 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput) // let the host process start: await waitingForChanges.WaitAsync(); - await Launch(serviceProjectA); + // service should have been created before Hot Reload session started: + Assert.NotNull(service); + + await Launch(serviceProjectA, service, workingDirectory, watchCancellationSource.Token); UpdateSourceFile(libSource, """ @@ -323,7 +342,7 @@ public static void Common() await updatesApplied.WaitAsync(); await updatesApplied.WaitAsync(); - await Launch(serviceProjectB); + await Launch(serviceProjectB, service, workingDirectory, watchCancellationSource.Token); // ServiceB received updates: await updatesApplied.WaitAsync(); @@ -338,32 +357,6 @@ public static void Common() catch (OperationCanceledException) { } - - async Task Launch(string projectPath) - { - // service should have been created before Hot Reload session started: - Assert.NotNull(service); - - var processTerminationSource = new CancellationTokenSource(); - var projectOptions = new ProjectOptions() - { - IsRootProject = false, - ProjectPath = projectPath, - WorkingDirectory = workingDirectory, - BuildProperties = [], - Command = "run", - CommandArguments = ["--project", projectPath], - LaunchEnvironmentVariables = [], - LaunchProfileName = null, - NoLaunchProfile = true, - TargetFramework = null, - }; - - var runningProject = await service.ProjectLauncher.TryLaunchProcessAsync(projectOptions, processTerminationSource, build: false, watchCancellationSource.Token); - Assert.NotNull(runningProject); - - await runningProject.WaitForProcessRunningAsync(CancellationToken.None); - } } public enum UpdateLocation @@ -417,9 +410,9 @@ public async Task HostRestart(UpdateLocation updateLocation) var restartRequested = reporter.RegisterSemaphore(MessageDescriptor.RestartRequested); var hasUpdate = new SemaphoreSlim(initialCount: 0); - reporter.OnProcessOutput += (projectPath, output) => + reporter.OnProjectProcessOutput += (projectPath, line) => { - if (output.Contains("")) + if (line.Content.Contains("")) { if (projectPath == hostProject) { @@ -489,4 +482,79 @@ public static void Print() { } } + + [Fact] + public async Task RudeEditInProjectWithoutRunningProcess() + { + var testAsset = TestAssets.CopyTestAsset("WatchAppMultiProc") + .WithSource(); + + var workingDirectory = testAsset.Path; + var hostDir = Path.Combine(testAsset.Path, "Host"); + var hostProject = Path.Combine(hostDir, "Host.csproj"); + var serviceDirA = Path.Combine(testAsset.Path, "ServiceA"); + var serviceSourceA2 = Path.Combine(serviceDirA, "A2.cs"); + var serviceProjectA = Path.Combine(serviceDirA, "A.csproj"); + + var console = new TestConsole(Logger); + var reporter = new TestReporter(Logger); + + var program = Program.TryCreate( + TestOptions.GetCommandLineOptions(["--verbose", "--non-interactive", "--project", hostProject]), + console, + TestOptions.GetEnvironmentOptions(workingDirectory, TestContext.Current.ToolsetUnderTest.DotNetHostPath), + reporter, + out var errorCode); + + Assert.Equal(0, errorCode); + Assert.NotNull(program); + + TestRuntimeProcessLauncher? service = null; + var factory = new TestRuntimeProcessLauncher.Factory(s => + { + service = s; + }); + + var watcher = Assert.IsType(program.CreateWatcher(factory)); + + var watchCancellationSource = new CancellationTokenSource(); + var watchTask = watcher.WatchAsync(watchCancellationSource.Token); + + var waitingForChanges = reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); + + var changeHandled = reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var sessionStarted = reporter.RegisterSemaphore(MessageDescriptor.HotReloadSessionStarted); + + // let the host process start: + await waitingForChanges.WaitAsync(); + + // service should have been created before Hot Reload session started: + Assert.NotNull(service); + + var runningProject = await Launch(serviceProjectA, service, workingDirectory, watchCancellationSource.Token); + await sessionStarted.WaitAsync(); + + // Terminate the process: + await service.ProjectLauncher.TerminateProcessAsync(runningProject, CancellationToken.None); + + // rude edit in A (changing assembly level attribute): + UpdateSourceFile(serviceSourceA2, """ + [assembly: System.Reflection.AssemblyMetadata("TestAssemblyMetadata", "2")] + """); + + await changeHandled.WaitAsync(); + + reporter.ProcessOutput.Contains("verbose ⌚ Rude edits detected but do not affect any running process"); + reporter.ProcessOutput.Contains($"verbose ❌ {serviceSourceA2}(1,12): error ENC0003: Updating 'attribute' requires restarting the application."); + + // clean up: + watchCancellationSource.Cancel(); + try + { + await watchTask; + } + catch (OperationCanceledException) + { + } + } } diff --git a/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs b/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs index 6b0aa27c851a..004e75749db3 100644 --- a/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs +++ b/test/dotnet-watch.Tests/MSBuildEvaluationFilterTest.cs @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.Watcher.Tools { public class MSBuildEvaluationFilterTest { - private static readonly EvaluationResult s_emptyEvaluationResult = new(new Dictionary()); + private static readonly EvaluationResult s_emptyEvaluationResult = new(new Dictionary(), projectGraph: null); [Fact] public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges() @@ -90,11 +90,13 @@ public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIs // There's a chance that the watcher does not correctly report edits to msbuild files on // concurrent edits. MSBuildEvaluationFilter uses timestamps to additionally track changes to these files. - var result = new EvaluationResult(new Dictionary() - { - { "Controlller.cs", new FileItem { FilePath = "Controlller.cs" } }, - { "Proj.csproj", new FileItem { FilePath = "Proj.csproj" } }, - }); + var result = new EvaluationResult( + new Dictionary() + { + { "Controlller.cs", new FileItem { FilePath = "Controlller.cs" } }, + { "Proj.csproj", new FileItem { FilePath = "Proj.csproj" } }, + }, + projectGraph: null); var fileSetFactory = new MockFileSetFactory() { TryCreateImpl = () => result }; diff --git a/test/dotnet-watch.Tests/MsBuildFileSetFactoryTest.cs b/test/dotnet-watch.Tests/MsBuildFileSetFactoryTest.cs index 076c0c982d7a..41b03dbd8876 100644 --- a/test/dotnet-watch.Tests/MsBuildFileSetFactoryTest.cs +++ b/test/dotnet-watch.Tests/MsBuildFileSetFactoryTest.cs @@ -1,23 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; +using Xunit.Sdk; namespace Microsoft.DotNet.Watcher.Tools { - public class MsBuildFileSetFactoryTest + public class MsBuildFileSetFactoryTest(ITestOutputHelper output) { - private readonly IReporter _reporter; - private readonly TestAssetsManager _testAssets; - private readonly string _muxerPath; + private readonly TestReporter _reporter = new(output); + private readonly TestAssetsManager _testAssets = new(output); - public MsBuildFileSetFactoryTest(ITestOutputHelper output) - { - _reporter = new TestReporter(output); - _testAssets = new TestAssetsManager(output); - _muxerPath = TestContext.Current.ToolsetUnderTest.DotNetHostPath; - } + private string MuxerPath + => TestContext.Current.ToolsetUnderTest.DotNetHostPath; private static string InspectPath(string path, string rootDir) => path.Substring(rootDir.Length + 1).Replace("\\", "/"); @@ -35,7 +33,7 @@ public async Task FindsCustomWatchItems() TargetFrameworks = ToolsetInfo.CurrentTargetFramework, }); - project.WithProjectChanges(d => d.Root.Add(XElement.Parse( + project.WithProjectChanges(d => d.Root!.Add(XElement.Parse( @" "))); @@ -71,7 +69,7 @@ public async Task ExcludesDefaultItemsWithWatchFalseMetadata() }, }); - project.WithProjectChanges(d => d.Root.Add(XElement.Parse( + project.WithProjectChanges(d => d.Root!.Add(XElement.Parse( @" "))); @@ -138,7 +136,7 @@ public async Task MultiTfm() }, }); - project.WithProjectChanges(d => d.Root.Add(XElement.Parse( + project.WithProjectChanges(d => d.Root!.Add(XElement.Parse( $@" @@ -199,28 +197,36 @@ public async Task IncludesContentFiles() public async Task IncludesContentFilesFromRCL() { var testDir = _testAssets.CreateTestDirectory(); - WriteFile(testDir, Path.Combine("RCL1", "RCL1.csproj"), -@" - - netcoreapp5.0 - - -"); + WriteFile( + testDir, + Path.Combine("RCL1", "RCL1.csproj"), + $""" + + + netstandard2.1 + + + """); + WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "css", "app.css")); WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "js", "site.js")); WriteFile(testDir, Path.Combine("RCL1", "wwwroot", "favicon.ico")); - var projectPath = WriteFile(testDir, Path.Combine("Project1", "Project1.csproj"), -@" - - netstandard2.1 - - - - -"); - WriteFile(testDir, Path.Combine("Project1", "Program.cs")); + var projectPath = WriteFile( + testDir, + Path.Combine("Project1", "Project1.csproj"), + """ + + + netstandard2.1 + + + + + + """); + WriteFile(testDir, Path.Combine("Project1", "Program.cs")); var result = await Evaluate(projectPath); @@ -244,7 +250,7 @@ public async Task ProjectReferences_OneLevel() { var project2 = _testAssets.CreateTestProject(new TestProject("Project2") { - TargetFrameworks = "netstandard2.1", + TargetFrameworks = "netstandard2.0", }); var project1 = _testAssets.CreateTestProject(new TestProject("Project1") @@ -273,19 +279,19 @@ public async Task TransitiveProjectReferences_TwoLevels() { var project3 = _testAssets.CreateTestProject(new TestProject("Project3") { - TargetFrameworks = "netstandard2.1", + TargetFrameworks = "netstandard2.0", }); var project2 = _testAssets.CreateTestProject(new TestProject("Project2") { - TargetFrameworks = "netstandard2.1", - ReferencedProjects = { project3.TestProject, }, + TargetFrameworks = "netstandard2.0", + ReferencedProjects = { project3.TestProject }, }); var project1 = _testAssets.CreateTestProject(new TestProject("Project1") { TargetFrameworks = $"{ToolsetInfo.CurrentTargetFramework};net462", - ReferencedProjects = { project2.TestProject, }, + ReferencedProjects = { project2.TestProject }, }); var result = await Evaluate(project1); @@ -323,19 +329,17 @@ public async Task ProjectReferences_Graph() .Path; var projectA = Path.Combine(testDirectory, "A", "A.csproj"); - var output = new OutputSink(); var options = new EnvironmentOptions( - MuxerPath: _muxerPath, + MuxerPath: MuxerPath, WorkingDirectory: testDirectory); - var filesetFactory = new MSBuildFileSetFactory(projectA, targetFramework: null, buildProperties: null, options, _reporter, output, trace: true); + var output = new List(); + _reporter.OnProcessOutput += line => output.Add(line.Content); - var result = await filesetFactory.TryCreateAsync(CancellationToken.None); - Assert.NotNull(result); + var filesetFactory = new MSBuildFileSetFactory(projectA, targetFramework: null, buildProperties: [("_DotNetWatchTraceOutput", "true")], options, _reporter); - _reporter.Output(string.Join( - Environment.NewLine, - output.Current.Lines.Select(l => "Sink output: " + l))); + var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); + Assert.NotNull(result); AssertEx.SequenceEqual( [ @@ -357,11 +361,50 @@ public async Task ProjectReferences_Graph() ], Inspect(testDirectory, result.Files)); // ensure each project is only visited once for collecting watch items - Assert.All( - ["A", "B", "C", "D", "E", "F", "G"], - projectName => - Assert.Single(output.Current.Lines, - line => line.Contains($"Collecting watch items from '{projectName}'"))); + AssertEx.SequenceEqual( + [ + "Collecting watch items from 'A'", + "Collecting watch items from 'B'", + "Collecting watch items from 'C'", + "Collecting watch items from 'D'", + "Collecting watch items from 'E'", + "Collecting watch items from 'F'", + "Collecting watch items from 'G'", + ], + output.Where(l => l.Contains("Collecting watch items from")).Select(l => l.Trim()).Order()); + } + + [Fact] + public async Task MsbuildOutput() + { + var project2 = _testAssets.CreateTestProject(new TestProject("Project2") + { + TargetFrameworks = "netstandard2.1", + }); + + var project1 = _testAssets.CreateTestProject(new TestProject("Project1") + { + TargetFrameworks = $"net462", + ReferencedProjects = { project2.TestProject, }, + }); + + var project1Path = GetTestProjectPath(project1); + + var options = new EnvironmentOptions( + MuxerPath: MuxerPath, + WorkingDirectory: Path.GetDirectoryName(project1Path)!); + + var output = new List(); + _reporter.OnProcessOutput += line => output.Add($"{(line.IsError ? "[stderr]" : "[stdout]")} {line.Content}"); + + var factory = new MSBuildFileSetFactory(project1Path, targetFramework: null, buildProperties: [], options, _reporter); + var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); + Assert.Null(result); + + // note: msbuild prints errors to stdout: + AssertEx.Equal( + $"[stdout] {project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)", + output.Single(l => l.Contains("error NU1201"))); } private Task Evaluate(TestAsset projectPath) @@ -370,11 +413,11 @@ private Task Evaluate(TestAsset projectPath) private async Task Evaluate(string projectPath) { var options = new EnvironmentOptions( - MuxerPath: _muxerPath, - WorkingDirectory: Path.GetDirectoryName(projectPath)); + MuxerPath: MuxerPath, + WorkingDirectory: Path.GetDirectoryName(projectPath)!); - var factory = new MSBuildFileSetFactory(projectPath, targetFramework: null, buildProperties: null, options, _reporter, new OutputSink(), trace: false); - var result = await factory.TryCreateAsync(CancellationToken.None); + var factory = new MSBuildFileSetFactory(projectPath, targetFramework: null, buildProperties: [], options, _reporter); + var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); Assert.NotNull(result); return result; } @@ -384,7 +427,7 @@ private async Task Evaluate(string projectPath) private static string WriteFile(TestAsset testAsset, string name, string contents = "") { var path = Path.Combine(GetTestProjectDirectory(testAsset), name); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, contents); return path; @@ -393,7 +436,7 @@ private static string WriteFile(TestAsset testAsset, string name, string content private static string WriteFile(TestDirectory testAsset, string name, string contents = "") { var path = Path.Combine(testAsset.Path, name); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, contents); return path; diff --git a/test/dotnet-watch.Tests/Utilities/AssertEx.cs b/test/dotnet-watch.Tests/Utilities/AssertEx.cs index 86e8a46d165c..311479a1056a 100644 --- a/test/dotnet-watch.Tests/Utilities/AssertEx.cs +++ b/test/dotnet-watch.Tests/Utilities/AssertEx.cs @@ -226,13 +226,16 @@ public static void EqualFileList(IEnumerable expectedFiles, IEnumerable< public static void Contains(string expected, IEnumerable items) { - if (items.Any(item => item == expected)) + if (items.Any(item => item.Contains(expected))) { return; } var message = new StringBuilder(); - message.AppendLine($"'{expected}' not found in:"); + message.AppendLine($"Expected output not found:"); + message.AppendLine(expected); + message.AppendLine(); + message.AppendLine("Actual output:"); foreach (var item in items) { diff --git a/test/dotnet-watch.Tests/Utilities/MockFileSetFactory.cs b/test/dotnet-watch.Tests/Utilities/MockFileSetFactory.cs index 4e59f9770e0a..0fa8f0d6aa9b 100644 --- a/test/dotnet-watch.Tests/Utilities/MockFileSetFactory.cs +++ b/test/dotnet-watch.Tests/Utilities/MockFileSetFactory.cs @@ -8,14 +8,12 @@ namespace Microsoft.DotNet.Watcher.Tools; internal class MockFileSetFactory() : MSBuildFileSetFactory( rootProjectFile: "test.csproj", targetFramework: null, - buildProperties: null, + buildProperties: [], new EnvironmentOptions(Environment.CurrentDirectory, "dotnet"), - NullReporter.Singleton, - outputSink: null, - trace: false) + NullReporter.Singleton) { public Func TryCreateImpl; - public override ValueTask TryCreateAsync(CancellationToken cancellationToken) + public override ValueTask TryCreateAsync(bool? requireProjectGraph, CancellationToken cancellationToken) => ValueTask.FromResult(TryCreateImpl?.Invoke()); } diff --git a/test/dotnet-watch.Tests/Utilities/MockReporter.cs b/test/dotnet-watch.Tests/Utilities/MockReporter.cs index 105cacb1b634..9edeb48b3d4b 100644 --- a/test/dotnet-watch.Tests/Utilities/MockReporter.cs +++ b/test/dotnet-watch.Tests/Utilities/MockReporter.cs @@ -3,6 +3,8 @@ #nullable enable +using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools; @@ -11,9 +13,12 @@ internal class MockReporter : IReporter { public readonly List Messages = []; - public bool ReportProcessOutput => false; + public bool EnableProcessOutputReporting => false; - public void ProcessOutput(string projectPath, string data) + public void ReportProcessOutput(OutputLine line) + => throw new InvalidOperationException(); + + public void ReportProcessOutput(ProjectGraphNode project, OutputLine line) => throw new InvalidOperationException(); public void Report(MessageDescriptor descriptor, string prefix, object?[] args) diff --git a/test/dotnet-watch.Tests/Utilities/TestReporter.cs b/test/dotnet-watch.Tests/Utilities/TestReporter.cs index 86ff9d62f1f4..e7054f2ed195 100644 --- a/test/dotnet-watch.Tests/Utilities/TestReporter.cs +++ b/test/dotnet-watch.Tests/Utilities/TestReporter.cs @@ -4,22 +4,39 @@ #nullable enable using System.Diagnostics; +using Microsoft.Build.Graph; +using Microsoft.DotNet.Watcher; +using Microsoft.DotNet.Watcher.Internal; namespace Microsoft.Extensions.Tools.Internal { internal class TestReporter(ITestOutputHelper output) : IReporter { private readonly Dictionary _actions = []; + public readonly List ProcessOutput = []; - public bool ReportProcessOutput + public bool EnableProcessOutputReporting => true; - public event Action? OnProcessOutput; + public event Action? OnProjectProcessOutput; + public event Action? OnProcessOutput; - public void ProcessOutput(string projectPath, string data) + public void ReportProcessOutput(OutputLine line) { - output.WriteLine($"[{Path.GetFileName(projectPath)}]: {data}"); - OnProcessOutput?.Invoke(projectPath, data); + output.WriteLine(line.Content); + ProcessOutput.Add(line.Content); + + OnProcessOutput?.Invoke(line); + } + + public void ReportProcessOutput(ProjectGraphNode project, OutputLine line) + { + var content = $"[{project.GetDisplayName()}]: {line.Content}"; + + output.WriteLine(content); + ProcessOutput.Add(content); + + OnProjectProcessOutput?.Invoke(project.ProjectInstance.FullPath, line); } public SemaphoreSlim RegisterSemaphore(MessageDescriptor descriptor) diff --git a/test/dotnet-watch.Tests/Utilities/TestRuntimeProcessLauncher.cs b/test/dotnet-watch.Tests/Utilities/TestRuntimeProcessLauncher.cs index de48ed6287ca..35e8fc249c56 100644 --- a/test/dotnet-watch.Tests/Utilities/TestRuntimeProcessLauncher.cs +++ b/test/dotnet-watch.Tests/Utilities/TestRuntimeProcessLauncher.cs @@ -20,12 +20,19 @@ public IRuntimeProcessLauncher TryCreate(ProjectGraphNode projectNode, ProjectLa } public Func>? GetEnvironmentVariablesImpl; + public Action? TerminateLaunchedProcessesImpl; public ProjectLauncher ProjectLauncher { get; } = projectLauncher; public ValueTask DisposeAsync() => ValueTask.CompletedTask; - public ValueTask> GetEnvironmentVariablesAsync(CancellationToken cancelToken) - => ValueTask.FromResult(GetEnvironmentVariablesImpl?.Invoke() ?? []); + public IEnumerable<(string name, string value)> GetEnvironmentVariables() + => GetEnvironmentVariablesImpl?.Invoke() ?? []; + + public ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancellationToken) + { + TerminateLaunchedProcessesImpl?.Invoke(); + return ValueTask.CompletedTask; + } } diff --git a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs index 087a1ce74c49..a17a7f8e2294 100644 --- a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs +++ b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs @@ -22,8 +22,14 @@ public async Task LaunchesBrowserOnStart() App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); + // check that all app output is printed out: + await App.AssertOutputLine(line => line.Contains("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")); + // Verify we launched the browser. - await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: https://localhost:5001/"); + Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001/")); } [Fact] diff --git a/test/dotnet-watch.Tests/Watch/ProgramTests.cs b/test/dotnet-watch.Tests/Watch/ProgramTests.cs index 2606ef744d2b..cb22f2cc4396 100644 --- a/test/dotnet-watch.Tests/Watch/ProgramTests.cs +++ b/test/dotnet-watch.Tests/Watch/ProgramTests.cs @@ -236,5 +236,31 @@ public async Task BuildCommand() await App.AssertOutputLine(line => line.Contains("warning : The value of property is '123'", StringComparison.Ordinal)); } + + [Fact] + public async Task ProjectGraphLoadFailure() + { + var testAsset = TestAssets + .CopyTestAsset("WatchAppWithProjectDeps") + .WithSource() + .WithProjectChanges((path, proj) => + { + if (Path.GetFileName(path) == "App.WithDeps.csproj") + { + proj.Root.Descendants() + .Single(e => e.Name.LocalName == "ItemGroup") + .Add(XElement.Parse(""" + + """)); + } + }); + + App.Start(testAsset, [], "AppWithDeps"); + + await App.AssertOutputLineStartsWith("dotnet watch ⌚ Fix the error to continue or press Ctrl+C to exit."); + + App.AssertOutputContains(@"dotnet watch ⌚ Failed to load project graph."); + App.AssertOutputContains($"dotnet watch ❌ The project file could not be loaded. Could not find a part of the path '{Path.Combine(testAsset.Path, "AppWithDeps", "NonExistentDirectory", "X.csproj")}'"); + } } } diff --git a/test/dotnet-watch.Tests/Watch/Utilities/WatchableApp.cs b/test/dotnet-watch.Tests/Watch/Utilities/WatchableApp.cs index b79829dfa6ed..c6a0da60a072 100644 --- a/test/dotnet-watch.Tests/Watch/Utilities/WatchableApp.cs +++ b/test/dotnet-watch.Tests/Watch/Utilities/WatchableApp.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Watcher.Tests { - internal sealed class WatchableApp : IDisposable + internal sealed class WatchableApp(ITestOutputHelper logger) : IDisposable { // Test apps should output this message as soon as they start running: private const string StartedMessage = "Started"; @@ -18,27 +18,35 @@ internal sealed class WatchableApp : IDisposable private const string WatchErrorOutputEmoji = "❌"; private const string WatchFileChanged = "dotnet watch ⌚ File changed:"; - public readonly ITestOutputHelper Logger; - private bool _prepared; - - public WatchableApp(ITestOutputHelper logger) - { - Logger = logger; - } + public ITestOutputHelper Logger => logger; public AwaitableProcess Process { get; private set; } - public List DotnetWatchArgs { get; } = new() { "--verbose" }; + public List DotnetWatchArgs { get; } = ["--verbose", "/bl:DotnetRun.binlog"]; - public Dictionary EnvironmentVariables { get; } = new Dictionary(); + public Dictionary EnvironmentVariables { get; } = []; public bool UsePollingWatcher { get; set; } - public static string GetLinePrefix(MessageDescriptor descriptor) - => $"dotnet watch {descriptor.Emoji} {descriptor.Format}"; + public static string GetLinePrefix(MessageDescriptor descriptor, string projectDisplay = null) + => $"dotnet watch {descriptor.Emoji}{(projectDisplay != null ? $" [{projectDisplay}]" : "")} {descriptor.Format}"; + + public void AssertOutputContains(string message) + => AssertEx.Contains(message, Process.Output); + + public void AssertOutputContains(MessageDescriptor descriptor, string projectDisplay = null) + => AssertOutputContains(GetLinePrefix(descriptor, projectDisplay)); + + public async ValueTask WaitUntilOutputContains(string message) + { + if (!Process.Output.Any(line => line.Contains(message))) + { + _ = await AssertOutputLine(line => line.Contains(message)); + } + } - public Task AssertOutputLineStartsWith(MessageDescriptor descriptor, Predicate failure = null) - => AssertOutputLineStartsWith(GetLinePrefix(descriptor), failure); + public Task AssertOutputLineStartsWith(MessageDescriptor descriptor, string projectDisplay = null, Predicate failure = null) + => AssertOutputLineStartsWith(GetLinePrefix(descriptor, projectDisplay), failure); /// /// Asserts that the watched process outputs a line starting with and returns the remainder of that line. @@ -97,25 +105,10 @@ public Task AssertFileChanged() public Task AssertExiting() => AssertOutputLineStartsWith(ExitingMessage); - private void Prepare(string projectDirectory) - { - if (_prepared) - { - return; - } - - var buildCommand = new BuildCommand(Logger, projectDirectory); - buildCommand.Execute().Should().Pass(); - - _prepared = true; - } - public void Start(TestAsset asset, IEnumerable arguments, string relativeProjectDirectory = null, string workingDirectory = null, TestFlags testFlags = TestFlags.RunningAsTest) { var projectDirectory = (relativeProjectDirectory != null) ? Path.Combine(asset.Path, relativeProjectDirectory) : asset.Path; - Prepare(projectDirectory); - var commandSpec = new DotnetCommand(Logger, ["watch", .. DotnetWatchArgs, .. arguments]) { WorkingDirectory = workingDirectory ?? projectDirectory,