| File: DotnetToolTask.cs | Web Access |
| Project: src\sdk\src\RazorSdk\Tasks\Microsoft.NET.Sdk.Razor.Tasks.csproj (Microsoft.NET.Sdk.Razor.Tasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using System.Diagnostics; using System.IO.Pipes; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.NET.Sdk.Razor.Tool; namespace Microsoft.AspNetCore.Razor.Tasks { public abstract class DotNetToolTask : ToolTask { // From https://github.com/dotnet/corefx/blob/29cd6a0b0ac2993cee23ebaf36ca3d4bce6dd75f/src/System.IO.Pipes/ref/System.IO.Pipes.cs#L93. // Using the enum value directly as this option is not available in netstandard. private const PipeOptions PipeOptionCurrentUserOnly = (PipeOptions)536870912; private string _dotnetPath; private CancellationTokenSource _razorServerCts; public bool Debug { get; set; } public bool DebugTool { get; set; } [Required] public string ToolAssembly { get; set; } public bool UseServer { get; set; } // Specifies whether we should fallback to in-process execution if server execution fails. public bool ForceServer { get; set; } // Specifies whether server execution is allowed when PipeOptions.CurrentUserOnly is not available. // For testing purposes only. public bool SuppressCurrentUserOnlyPipeOptions { get; set; } public string PipeName { get; set; } protected override string ToolName => "dotnet"; // If we're debugging then make all of the stdout gets logged in MSBuild protected override MessageImportance StandardOutputLoggingImportance => DebugTool ? MessageImportance.High : base.StandardOutputLoggingImportance; protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High; internal abstract string Command { get; } protected override string GenerateFullPathToTool() => DotNetPath; private string DotNetPath { get { if (!string.IsNullOrEmpty(_dotnetPath)) { return _dotnetPath; } var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); _dotnetPath = string.IsNullOrEmpty(dotnetHostPath) ? ToolExe : dotnetHostPath; return _dotnetPath; } } protected override string GenerateCommandLineCommands() { return $"exec \"{ToolAssembly}\"" + (DebugTool ? " --debug" : ""); } protected override string GetResponseFileSwitch(string responseFilePath) { return "@\"" + responseFilePath + "\""; } protected abstract override string GenerateResponseFileCommands(); public override bool Execute() { if (Debug) { #if NET5_0_OR_GREATER var processId = Environment.ProcessId; #else var processId = Process.GetCurrentProcess().Id; #endif Log.LogMessage(MessageImportance.High, "Waiting for debugger in pid: {0}", processId); while (!Debugger.IsAttached) { Thread.Sleep(TimeSpan.FromSeconds(3)); } } return base.Execute(); } protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) { if (UseServer && TryExecuteOnServer(pathToTool, responseFileCommands, commandLineCommands, out var result)) { return result; } return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); } protected override void LogToolCommand(string message) { if (Debug) { Log.LogMessage(MessageImportance.High, message); } else { base.LogToolCommand(message); } } public override void Cancel() { base.Cancel(); _razorServerCts?.Cancel(); } protected virtual bool TryExecuteOnServer( string pathToTool, string responseFileCommands, string commandLineCommands, out int result) { #if !NETFRAMEWORK if (!SuppressCurrentUserOnlyPipeOptions && !Enum.IsDefined(typeof(PipeOptions), PipeOptionCurrentUserOnly)) { // For security reasons, we don't want to spin up a server that doesn't // restrict requests only to the current user. result = -1; return ForceServer; } #endif Log.LogMessage(StandardOutputLoggingImportance, "Server execution started."); using (_razorServerCts = new CancellationTokenSource()) { Log.LogMessage(StandardOutputLoggingImportance, $"CommandLine = '{commandLineCommands}'"); Log.LogMessage(StandardOutputLoggingImportance, $"ServerResponseFile = '{responseFileCommands}'"); // The server contains the tools for discovering tag helpers and generating Razor code. var clientDir = Path.GetFullPath(Path.GetDirectoryName(ToolAssembly)); var workingDir = CurrentDirectoryToUse(); var tempDir = ServerConnection.GetTempPath(workingDir); var serverPaths = new ServerPaths( clientDir, workingDir: workingDir, tempDir: tempDir); var arguments = GetArguments(responseFileCommands); var responseTask = ServerConnection.RunOnServer(PipeName, arguments, serverPaths, _razorServerCts.Token, debug: DebugTool); responseTask.Wait(_razorServerCts.Token); var response = responseTask.Result; if (response.Type == ServerResponse.ResponseType.Completed && response is CompletedServerResponse completedResponse) { result = completedResponse.ReturnCode; if (result == 0) { // Server execution succeeded. Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}."); // There might still be warnings in the error output. if (LogStandardErrorAsError) { LogErrors(completedResponse.ErrorOutput); } else { LogMessages(completedResponse.ErrorOutput, StandardErrorLoggingImportance); } return true; } else if (result == 2) { // Server execution completed with a legit error. No need to fallback to cli execution. Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable."); if (LogStandardErrorAsError) { LogErrors(completedResponse.ErrorOutput); } else { LogMessages(completedResponse.ErrorOutput, StandardErrorLoggingImportance); } return true; } else { // Server execution completed with an error but we still want to fallback to cli execution. Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable."); } } else { // Server execution failed. Fallback to cli execution. Log.LogMessage( StandardOutputLoggingImportance, $"Server execution failed with response {response.Type}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable."); result = -1; } if (ForceServer) { // We don't want to fallback to in-process execution. return true; } Log.LogMessage(StandardOutputLoggingImportance, "Fallback to in-process execution."); } return false; } private void LogMessages(string output, MessageImportance messageImportance) { var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (!string.IsNullOrWhiteSpace(line)) { var trimmedMessage = line.Trim(); Log.LogMessageFromText(trimmedMessage, messageImportance); } } } private void LogErrors(string output) { var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (!string.IsNullOrWhiteSpace(line)) { var trimmedMessage = line.Trim(); Log.LogError(trimmedMessage); } } } /// <summary> /// Get the current directory that the compiler should run in. /// </summary> private string CurrentDirectoryToUse() { // ToolTask has a method for this. But it may return null. Use the process directory // if ToolTask didn't override. MSBuild uses the process directory. var workingDirectory = GetWorkingDirectory(); if (string.IsNullOrEmpty(workingDirectory)) { workingDirectory = Directory.GetCurrentDirectory(); } return workingDirectory; } private IList<string> GetArguments(string responseFileCommands) { var list = responseFileCommands.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); return list; } protected override bool HandleTaskExecutionErrors() { if (!HasLoggedErrors) { var toolCommand = Path.GetFileNameWithoutExtension(ToolAssembly) + " " + Command; // Show a slightly better error than the standard ToolTask message that says "dotnet" failed. Log.LogError($"{toolCommand} exited with code {ExitCode}."); return false; } return base.HandleTaskExecutionErrors(); } } }