diff --git a/src/Build.UnitTests/MSBuildTaskHostTests.cs b/src/Build.UnitTests/MSBuildTaskHostTests.cs new file mode 100644 index 00000000000..2891387495f --- /dev/null +++ b/src/Build.UnitTests/MSBuildTaskHostTests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.Engine.UnitTests; + +public class MSBuildTaskHostTests(ITestOutputHelper testOutput) : IDisposable +{ + private static string AssemblyLocation + => field ??= Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? AppContext.BaseDirectory); + + private static string TestAssetsRootPath + => field ??= Path.Combine(AssemblyLocation, "TestAssets"); + + private readonly TestEnvironment _environment = TestEnvironment.Create(testOutput); + + public void Dispose() + => _environment.Dispose(); + + [WindowsNet35OnlyFact] + public void CompileNet35WinFormsApp() + { + TransientTestFolder testFolder = _environment.CreateFolder(createFolder: true); + + CopyFilesRecursively(Path.Combine(TestAssetsRootPath, "Net35WinFormsApp"), testFolder.Path); + string projectFilePath = Path.Combine(testFolder.Path, "TestNet35WinForms.csproj"); + + _ = RunnerUtilities.ExecBootstrapedMSBuild($"{projectFilePath}", out bool success, outputHelper: testOutput); + success.ShouldBeTrue(); + } + + private static void CopyFilesRecursively(string sourcePath, string targetPath) + { + // First Create all directories + foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); + } + + // Then copy all the files & Replaces any files with the same name + foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories)) + { + File.Copy(newPath, newPath.Replace(sourcePath, targetPath), overwrite: true); + } + } +} diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 9cad26fc463..5149275192f 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -1,4 +1,4 @@ - + $(RuntimeOutputTargetFrameworks) @@ -8,13 +8,15 @@ $(DefineConstants);MICROSOFT_BUILD_ENGINE_UNITTESTS - + $(DefineConstants);NO_MSBUILDTASKHOST true - + $(NoWarn);MSB3270;CS0436 + + ..\Shared\UnitTests\App.config @@ -30,7 +32,7 @@ all - + @@ -54,6 +56,13 @@ + + + + + + + @@ -77,9 +86,6 @@ Collections\CopyOnWriteDictionary_Tests.cs - - Collections\ImmutableDictionary_Tests.cs - @@ -89,6 +95,7 @@ PreserveNewest + PreserveNewest @@ -125,7 +132,7 @@ PreserveNewest - + diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/App.config b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/App.config new file mode 100644 index 00000000000..343984d02a5 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.Designer.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.Designer.cs new file mode 100644 index 00000000000..7444d24def3 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.Designer.cs @@ -0,0 +1,56 @@ +namespace TestNet35WinForms +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); + this.button1 = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // button1 + // + resources.ApplyResources(this.button1, "button1"); + this.button1.Name = "button1"; + this.button1.UseVisualStyleBackColor = true; + // + // Form1 + // + resources.ApplyResources(this, "$this"); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.button1); + this.Name = "Form1"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button button1; + } +} + diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.cs new file mode 100644 index 00000000000..573a142d2d5 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.cs @@ -0,0 +1,12 @@ +using System.Windows.Forms; + +namespace TestNet35WinForms +{ + public partial class Form1 : Form + { + public Form1() + { + InitializeComponent(); + } + } +} diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.resx b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.resx new file mode 100644 index 00000000000..c4dc6763d4b --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Form1.resx @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + 0 + + + 0 + + + + 222, 133 + + + button1 + + + Form1 + + + button1 + + + System.Windows.Forms.Button, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 6, 13 + + + 800, 450 + + + 223, 124 + + + System.Windows.Forms.Form, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Form1 + + + $this + + + True + + + fr + + \ No newline at end of file diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Program.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Program.cs new file mode 100644 index 00000000000..ad80aff934a --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Program.cs @@ -0,0 +1,19 @@ +using System; +using System.Windows.Forms; + +namespace TestNet35WinForms +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/AssemblyInfo.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..3714740b839 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TestNet35WinForms")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestNet35WinForms")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f8f9dbb8-cf1f-43fa-a6be-5200d903b1d8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.Designer.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.Designer.cs new file mode 100644 index 00000000000..fc045239cee --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TestNet35WinForms.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TestNet35WinForms.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.resx b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.resx new file mode 100644 index 00000000000..af7dbebbace --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.Designer.cs b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.Designer.cs new file mode 100644 index 00000000000..8534fdac326 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TestNet35WinForms.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.settings b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.settings new file mode 100644 index 00000000000..39645652af6 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Build.UnitTests/TestAssets/Net35WinFormsApp/TestNet35WinForms.csproj b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/TestNet35WinForms.csproj new file mode 100644 index 00000000000..0bc3550a967 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/Net35WinFormsApp/TestNet35WinForms.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {F8F9DBB8-CF1F-43FA-A6BE-5200D903B1D8} + WinExe + TestNet35WinForms + TestNet35WinForms + v3.5 + 512 + true + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + \ No newline at end of file diff --git a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs index c409c856c0b..e1e7414c8fd 100644 --- a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs @@ -2,9 +2,32 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.Build.Internal; namespace Microsoft.Build.BackEnd { + /// + /// Represents the configuration data needed to launch a node process. + /// + /// + /// The path to the MSBuild binary to launch (e.g., MSBuild.exe, MSBuild.dll or MSBuildTaskHost.exe). + /// If is passed, will + /// be used as the default MSBuild location. + /// + /// The command line arguments to pass to the executable. + /// The handshake data used to establish communication with the node process. + /// + /// if the dotnet.exe should be used to launch the MSBuild assembly; + /// if the MSBuild executable should be launched directly. + /// + internal readonly record struct NodeLaunchData( + string? MSBuildLocation, + string CommandLineArgs, + Handshake Handshake, + bool UsingDotNetExe = false) + { + } + internal interface INodeLauncher { Process Start(string msbuildLocation, string commandLineArgs, int nodeId); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index 9611b9850e6..81bc379ad4f 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -452,15 +453,16 @@ internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOption bool isArm64 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.Arm64); bool isCLR2 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2); - // Unsupported combinations - if (isArm64 && isCLR2) - { - ErrorUtilities.ThrowInternalError("ARM64 CLR2 task hosts are not supported."); - } - if (isCLR2) { - return isX64 ? Path.Combine(GetOrInitializeX64Clr2Path(toolName), toolName) : Path.Combine(GetOrInitializeX32Clr2Path(toolName), toolName); + if (isArm64) + { + ErrorUtilities.ThrowInternalError("ARM64 CLR2 task hosts are not supported."); + } + + return isX64 + ? Path.Combine(GetOrInitializeX64Clr2Path(toolName), toolName) + : Path.Combine(GetOrInitializeX32Clr2Path(toolName), toolName); } if (isX64) @@ -668,69 +670,96 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN // Create callbacks that capture the TaskHostNodeKey void OnNodeContextCreated(NodeContext context) => NodeContextCreated(context, nodeKey); - IList nodeContexts; + NodeLaunchData nodeLaunchData = ResolveNodeLaunchConfiguration(hostContext, in taskHostParameters); + + if (nodeLaunchData.MSBuildLocation == null) + { + return false; + } + + if (nodeLaunchData.UsingDotNetExe) + { + CommunicationsUtilities.Trace("For a host context of {0}, spawning dotnet.exe from {1}.", hostContext.ToString(), nodeLaunchData.MSBuildLocation); + } + else + { + CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext.ToString(), nodeLaunchData.MSBuildLocation); + } + + // There is always one task host per host context so we always create just 1 one task host node here. + IList nodeContexts = GetNodes( + nodeLaunchData.MSBuildLocation, + nodeLaunchData.CommandLineArgs, + communicationNodeId, + factory: this, + nodeLaunchData.Handshake, + OnNodeContextCreated, + NodeContextTerminated, + numberOfNodesToCreate: 1); + + return nodeContexts.Count == 1; + } + + private NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, ref readonly TaskHostParameters taskHostParameters) + { + string msbuildLocation; + string commandLineArgs; + Handshake handshake; + bool nodeReuse; + + BuildParameters buildParameters = ComponentHost.BuildParameters; - // Handle .NET task host context #if NETFRAMEWORK + + // Handle scenario where a .NET task host is launched from .NET Framework if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET)) { - (string runtimeHostPath, string msbuildAssemblyPath) = GetMSBuildLocationForNETRuntime(hostContext, taskHostParameters); - - CommunicationsUtilities.Trace("For a host context of {0}, spawning dotnet.exe from {1}.", hostContext.ToString(), runtimeHostPath); + (string runtimeHostPath, string msbuildAssemblyDirectory) = GetMSBuildLocationForNETRuntime(hostContext, taskHostParameters); - var handshake = new Handshake(hostContext, predefinedToolsDirectory: msbuildAssemblyPath); + msbuildLocation = Path.Combine(msbuildAssemblyDirectory, Constants.MSBuildAssemblyName); + nodeReuse = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse); - string commandLineArgs = $"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{NodeReuseIsEnabled(hostContext).ToString().ToLower()} /low:{ComponentHost.BuildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion}"; + commandLineArgs = $""" + "{msbuildLocation}" /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuse.ToString().ToLower()} /low:{buildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} + """; - // There is always one task host per host context so we always create just 1 one task host node here. - nodeContexts = GetNodes( - runtimeHostPath, - commandLineArgs, - communicationNodeId, - this, - handshake, - OnNodeContextCreated, - NodeContextTerminated, - 1); + handshake = new Handshake(hostContext, toolsDirectory: msbuildAssemblyDirectory); - return nodeContexts.Count == 1; + return new NodeLaunchData(runtimeHostPath, commandLineArgs, handshake, UsingDotNetExe: true); } #endif - string msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext); + msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext); // we couldn't even figure out the location we're trying to launch ... just go ahead and fail. if (msbuildLocation == null) { - return false; + return default; } - CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext.ToString(), msbuildLocation ?? Constants.MSBuildExecutableName); +#if FEATURE_NET35_TASKHOST + if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2)) + { + // The .NET 3.5 task host uses the directory of its EXE when calculating salt for the handshake. + string toolsDirectory = Path.GetDirectoryName(msbuildLocation) ?? string.Empty; + + // MSBuildTaskHost doesn't use command-line arguments. + commandLineArgs = ""; + handshake = new Handshake(hostContext, toolsDirectory); - string nonNetCommandLineArgs = $"/nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{NodeReuseIsEnabled(hostContext).ToString().ToLower()} /low:{ComponentHost.BuildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion}"; + return new NodeLaunchData(msbuildLocation, commandLineArgs, handshake); + } +#endif - nodeContexts = GetNodes( - msbuildLocation, - nonNetCommandLineArgs, - communicationNodeId, - this, - new Handshake(hostContext), - OnNodeContextCreated, - NodeContextTerminated, - 1); + nodeReuse = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse); - return nodeContexts.Count == 1; + commandLineArgs = $""" + /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuse.ToString().ToLower()} /low:{buildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} + """; - // Determines whether node reuse should be enabled for the given host context. - // Node reuse allows MSBuild to reuse existing task host processes for better performance, - // but is disabled for CLR2 because it uses legacy MSBuildTaskHost. - bool NodeReuseIsEnabled(HandshakeOptions hostContext) - { - bool isCLR2 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2); + handshake = new Handshake(hostContext); - return Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse) - && !isCLR2; - } + return new NodeLaunchData(msbuildLocation, commandLineArgs, handshake); } /// diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 149b8043e4f..95e68ae8409 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -14,7 +14,7 @@ $(DefineConstants);FEATURE_DEBUG_LAUNCH - + $(DefineConstants);FEATURE_APARTMENT_STATE $(DefineConstants);FEATURE_APM $(DefineConstants);FEATURE_APPDOMAIN diff --git a/src/MSBuildTaskHost/AssemblyInfo.cs b/src/MSBuildTaskHost/AssemblyInfo.cs deleted file mode 100644 index 191d7a3e309..00000000000 --- a/src/MSBuildTaskHost/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -global using NativeMethodsShared = Microsoft.Build.Framework.NativeMethods; - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Build.Engine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] diff --git a/src/MSBuildTaskHost/AssemblyResources.cs b/src/MSBuildTaskHost/AssemblyResources.cs deleted file mode 100644 index 6ee4f10b194..00000000000 --- a/src/MSBuildTaskHost/AssemblyResources.cs +++ /dev/null @@ -1,37 +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 System.Globalization; -using System.Reflection; -using System.Resources; - -#nullable disable - -namespace Microsoft.Build.Shared -{ - /// - /// This class provides access to the assembly's resources. - /// - internal static class AssemblyResources - { - /// - /// Actual source of the resource string we'll be reading. - /// - private static readonly ResourceManager s_resources = new ResourceManager("MSBuildTaskHost.Strings.Shared", Assembly.GetExecutingAssembly()); - - /// - /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. - /// - /// This method is thread-safe. - /// The resource string, or null if not found. - internal static string GetString(string name) - { - // NOTE: the ResourceManager.GetString() method is thread-safe - string resource = s_resources.GetString(name, CultureInfo.CurrentUICulture); - - ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); - - return resource; - } - } -} diff --git a/src/MSBuildTaskHost/BackEnd/BinaryReaderExtensions.cs b/src/MSBuildTaskHost/BackEnd/BinaryReaderExtensions.cs new file mode 100644 index 00000000000..a89ba126eca --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/BinaryReaderExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.TaskHost.BackEnd; + +internal static class BinaryReaderExtensions +{ + public static string? ReadOptionalString(this BinaryReader reader) + => reader.ReadByte() == 0 ? null : reader.ReadString(); + + public static int? ReadOptionalInt32(this BinaryReader reader) + => reader.ReadByte() == 0 ? null : reader.ReadInt32(); +} diff --git a/src/MSBuildTaskHost/BackEnd/BinaryReaderFactory.cs b/src/MSBuildTaskHost/BackEnd/BinaryReaderFactory.cs new file mode 100644 index 00000000000..f32c2b39c1b --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/BinaryReaderFactory.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Opaque holder of shared buffer. +/// +internal abstract class BinaryReaderFactory +{ + public abstract BinaryReader Create(Stream stream); +} diff --git a/src/MSBuildTaskHost/BackEnd/BinaryTranslator.cs b/src/MSBuildTaskHost/BackEnd/BinaryTranslator.cs new file mode 100644 index 00000000000..7e561ebf069 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/BinaryTranslator.cs @@ -0,0 +1,685 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using Microsoft.Build.TaskHost.Exceptions; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// This class is responsible for serializing and deserializing simple types to and +/// from the byte streams used to communicate INodePacket-implementing classes. +/// Each class implements a Translate method on INodePacket which takes this class +/// as a parameter, and uses it to store and retrieve fields to the stream. +/// +internal static class BinaryTranslator +{ + private static byte[] EmptyByteArray => field ??= []; + + /// + /// Returns a read-only serializer. + /// + /// The serializer. + internal static ITranslator GetReadTranslator(Stream stream, BinaryReaderFactory buffer) + => new BinaryReadTranslator(stream, buffer); + + /// + /// Returns a write-only serializer. + /// + /// The stream containing data to serialize. + /// The serializer. + internal static ITranslator GetWriteTranslator(Stream stream) + => new BinaryWriteTranslator(stream); + + /// + /// Implementation of ITranslator for reading from a stream. + /// + private class BinaryReadTranslator(Stream packetStream, BinaryReaderFactory buffer) : ITranslator + { + /// + /// Gets the reader, if any. + /// + public BinaryReader Reader { get; } = buffer.Create(packetStream); + + /// + /// Gets the writer, if any. + /// + public BinaryWriter Writer + { + get + { + ErrorUtilities.ThrowInternalError("Cannot get writer from reader."); + return null; + } + } + + /// + /// Gets the current serialization mode. + /// + public TranslationDirection Mode => TranslationDirection.ReadFromStream; + + /// + public byte NegotiatedPacketVersion { get; set; } + + /// + /// Delegates the Dispose call the to the underlying BinaryReader. + /// + public void Dispose() + => Reader.Close(); + + /// + /// Translates a boolean. + /// + /// The value to be translated. + public void Translate(ref bool value) + => value = Reader.ReadBoolean(); + + /// + /// Translates an array. + /// + /// The array to be translated. + public void Translate(ref bool[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = Reader.ReadInt32(); + array = new bool[count]; + + for (int i = 0; i < count; i++) + { + array[i] = Reader.ReadBoolean(); + } + } + + /// + /// Translates a byte. + /// + /// The value to be translated. + public void Translate(ref byte value) + => value = Reader.ReadByte(); + + /// + /// Translates a short. + /// + /// The value to be translated. + public void Translate(ref short value) + => value = Reader.ReadInt16(); + + /// + /// Translates an unsigned short. + /// + /// The value to be translated. + public void Translate(ref ushort value) + => value = Reader.ReadUInt16(); + + /// + /// Translates an integer. + /// + /// The value to be translated. + public void Translate(ref int value) + => value = Reader.ReadInt32(); + + /// + /// Translates an array. + /// + /// The array to be translated. + public void Translate(ref int[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = Reader.ReadInt32(); + array = new int[count]; + + for (int i = 0; i < count; i++) + { + array[i] = Reader.ReadInt32(); + } + } + + /// + /// Translates a long. + /// + /// The value to be translated. + public void Translate(ref long value) + => value = Reader.ReadInt64(); + + /// + /// Translates a double. + /// + /// The value to be translated. + public void Translate(ref double value) + => value = Reader.ReadDouble(); + + /// + /// Translates a string. + /// + /// The value to be translated. + public void Translate(ref string? value) + { + if (!TranslateNullable(value)) + { + return; + } + + value = Reader.ReadString(); + } + + /// + /// Translates a byte array. + /// + /// The array to be translated. + public void Translate(ref byte[]? byteArray) + { + if (!TranslateNullable(byteArray)) + { + return; + } + + int count = Reader.ReadInt32(); + byteArray = count > 0 + ? Reader.ReadBytes(count) + : EmptyByteArray; + } + + /// + /// Translates a string array. + /// + /// The array to be translated. + public void Translate(ref string[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = Reader.ReadInt32(); + array = new string[count]; + + for (int i = 0; i < count; i++) + { + array[i] = Reader.ReadString(); + } + } + + /// + /// Translates a collection of T into the specified type using an and . + /// + /// The collection to be translated. + /// The translator to use for the values in the collection. + /// The factory to create the ICollection. + /// The type contained in the collection. + /// The type of collection to be created. + public void Translate(ref ICollection? collection, ObjectTranslator objectTranslator, NodePacketCollectionCreator collectionFactory) + where L : ICollection + { + if (!TranslateNullable(collection)) + { + return; + } + + int count = Reader.ReadInt32(); + collection = collectionFactory(count); + + for (int i = 0; i < count; i++) + { + T value = default!; + objectTranslator(this, ref value); + collection.Add(value); + } + } + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + public void Translate(ref DateTime value) + { + DateTimeKind kind = DateTimeKind.Unspecified; + TranslateEnum(ref kind, 0); + value = new DateTime(Reader.ReadInt64(), kind); + } + + /// + /// Translates a CultureInfo. + /// + /// The CultureInfo to translate. + public void TranslateCulture(ref CultureInfo? value) + { + string cultureName = Reader.ReadString(); + + // It may be that some culture codes are accepted on later .net framework versions + // but not on the older 3.5 or 2.0. Fallbacks are required in this case to prevent + // exceptions + try + { + value = new CultureInfo(cultureName); + } + catch + { + value = CultureInfo.CurrentCulture; + } + } + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + public void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum + { + numericValue = Reader.ReadInt32(); + Type enumType = value.GetType(); + value = (T)Enum.ToObject(enumType, numericValue); + } + + public void TranslateException(ref Exception? value) + { + if (!TranslateNullable(value)) + { + return; + } + + value = BuildExceptionBase.ReadExceptionFromTranslator(this); + } + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + public void TranslateDictionary(ref Dictionary? dictionary, IEqualityComparer comparer) + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = Reader.ReadInt32(); + dictionary = new Dictionary(count, comparer); + + for (int i = 0; i < count; i++) + { + string? key = null; + Translate(ref key); + string? value = null; + Translate(ref value); + + // NOTE: This can throw if key is null. + dictionary[key!] = value; + } + } + + /// + public void TranslateDictionary( + ref Dictionary? dictionary, + IEqualityComparer comparer, + ObjectTranslatorWithValueFactory objectTranslator, + NodePacketValueFactory valueFactory) + where T : class + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = Reader.ReadInt32(); + dictionary = new Dictionary(count, comparer); + + for (int i = 0; i < count; i++) + { + string? key = null; + Translate(ref key); + T value = default!; + objectTranslator(this, valueFactory, ref value); + + // NOTE: This can throw if key is null. + dictionary[key!] = value; + } + } + + /// + /// Reads in the boolean which says if this object is null or not. + /// + /// The type of object to test. + /// True if the object should be read, false otherwise. + public bool TranslateNullable(T? value) + where T : class + { + bool haveRef = Reader.ReadBoolean(); + return haveRef; + } + } + + /// + /// Implementation of ITranslator for writing to a stream. + /// + /// The stream serving as the source or destination of data. + private class BinaryWriteTranslator(Stream packetStream) : ITranslator + { + /// + /// Gets the reader, if any. + /// + public BinaryReader Reader + { + get + { + ErrorUtilities.ThrowInternalError("Cannot get reader from writer."); + return null; + } + } + + /// + /// Gets the writer, if any. + /// + public BinaryWriter Writer { get; } = new BinaryWriter(packetStream); + + /// + /// Gets the current serialization mode. + /// + public TranslationDirection Mode => TranslationDirection.WriteToStream; + + /// + public byte NegotiatedPacketVersion { get; set; } + + /// + /// Delegates the Dispose call the to the underlying BinaryWriter. + /// + public void Dispose() + => Writer.Close(); + + /// + /// Translates a boolean. + /// + /// The value to be translated. + public void Translate(ref bool value) + => Writer.Write(value); + + /// + /// Translates an array. + /// + /// The array to be translated. + public void Translate(ref bool[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = array.Length; + Writer.Write(count); + + for (int i = 0; i < count; i++) + { + Writer.Write(array[i]); + } + } + + /// + /// Translates a byte. + /// + /// The value to be translated. + public void Translate(ref byte value) + => Writer.Write(value); + + /// + /// Translates a short. + /// + /// The value to be translated. + public void Translate(ref short value) + => Writer.Write(value); + + /// + /// Translates an unsigned short. + /// + /// The value to be translated. + public void Translate(ref ushort value) + => Writer.Write(value); + + /// + /// Translates an integer. + /// + /// The value to be translated. + public void Translate(ref int value) + => Writer.Write(value); + + /// + /// Translates an array. + /// + /// The array to be translated. + public void Translate(ref int[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = array.Length; + Writer.Write(count); + + for (int i = 0; i < count; i++) + { + Writer.Write(array[i]); + } + } + + /// + /// Translates a long. + /// + /// The value to be translated. + public void Translate(ref long value) + => Writer.Write(value); + + /// + /// Translates a double. + /// + /// The value to be translated. + public void Translate(ref double value) + => Writer.Write(value); + + /// + /// Translates a string. + /// + /// The value to be translated. + public void Translate(ref string? value) + { + if (!TranslateNullable(value)) + { + return; + } + + Writer.Write(value); + } + + /// + /// Translates a string array. + /// + /// The array to be translated. + public void Translate(ref string[]? array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = array.Length; + Writer.Write(count); + + for (int i = 0; i < count; i++) + { + Writer.Write(array[i]); + } + } + + /// + /// Translates a collection of T into the specified type using an and . + /// + /// The collection to be translated. + /// The translator to use for the values in the collection. + /// The factory to create the ICollection. + /// The type contained in the collection. + /// The type of collection to be created. + public void Translate( + ref ICollection? collection, + ObjectTranslator objectTranslator, + NodePacketCollectionCreator collectionFactory) + where L : ICollection + { + if (!TranslateNullable(collection)) + { + return; + } + + Writer.Write(collection.Count); + + foreach (T item in collection) + { + T value = item; + objectTranslator(this, ref value); + } + } + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + public void Translate(ref DateTime value) + { + DateTimeKind kind = value.Kind; + TranslateEnum(ref kind, (int)kind); + Writer.Write(value.Ticks); + } + + /// + /// Translates a CultureInfo. + /// + /// The CultureInfo. + public void TranslateCulture(ref CultureInfo? value) + => Writer.Write(value!.Name); + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + public void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum + => Writer.Write(numericValue); + + public void TranslateException(ref Exception? value) + { + if (!TranslateNullable(value)) + { + return; + } + + BuildExceptionBase.WriteExceptionToTranslator(this, value); + } + + /// + /// Translates a byte array. + /// + /// The byte array to be translated. + public void Translate(ref byte[]? byteArray) + { + if (!TranslateNullable(byteArray)) + { + return; + } + + int length = byteArray.Length; + + Writer.Write(length); + if (length > 0) + { + Writer.Write(byteArray, 0, length); + } + } + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + public void TranslateDictionary(ref Dictionary? dictionary, IEqualityComparer comparer) + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + Writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string? key = pair.Key; + Translate(ref key); + string? value = pair.Value; + Translate(ref value); + } + } + + /// + public void TranslateDictionary( + ref Dictionary? dictionary, + IEqualityComparer comparer, + ObjectTranslatorWithValueFactory objectTranslator, + NodePacketValueFactory valueFactory) + where T : class + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + Writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string? key = pair.Key; + Translate(ref key); + T value = pair.Value; + objectTranslator(this, valueFactory, ref value); + } + } + + /// + /// Writes out the boolean which says if this object is null or not. + /// + /// The object to test. + /// The type of object to test. + /// True if the object should be written, false otherwise. + public bool TranslateNullable([NotNullWhen(true)] T? value) + where T : class + { + bool haveRef = value != null; + Writer.Write(haveRef); + return haveRef; + } + } +} diff --git a/src/MSBuildTaskHost/BackEnd/BinaryWriterExtensions.cs b/src/MSBuildTaskHost/BackEnd/BinaryWriterExtensions.cs new file mode 100644 index 00000000000..0b1619bc1d2 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/BinaryWriterExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.TaskHost.BackEnd; + +internal static class BinaryWriterExtensions +{ + public static void WriteOptionalString(this BinaryWriter writer, string? value) + { + if (value == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(value); + } + } + + public static void WriteOptionalInt32(this BinaryWriter writer, int? value) + { + if (value == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(value.Value); + } + } +} diff --git a/src/MSBuildTaskHost/BackEnd/BufferedReadStream.cs b/src/MSBuildTaskHost/BackEnd/BufferedReadStream.cs new file mode 100644 index 00000000000..3343b74be91 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/BufferedReadStream.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.IO.Pipes; + +namespace Microsoft.Build.TaskHost.BackEnd; + +internal sealed class BufferedReadStream(NamedPipeServerStream innerStream) : Stream +{ + private const int BUFFER_SIZE = 1024; + + private readonly NamedPipeServerStream _innerStream = innerStream; + private readonly byte[] _buffer = new byte[BUFFER_SIZE]; + + // The number of bytes in the buffer that have been read from the underlying stream but not read by consumers of this stream + private int _currentlyBufferedByteCount = 0; + private int _currentIndexInBuffer; + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerStream.Dispose(); + } + + base.Dispose(disposing); + } + + public override void Flush() + => _innerStream.Flush(); + + public override int ReadByte() + { + if (_currentlyBufferedByteCount > 0) + { + int ret = _buffer[_currentIndexInBuffer]; + _currentIndexInBuffer++; + _currentlyBufferedByteCount--; + return ret; + } + else + { + // Let the base class handle it, which will end up calling the Read() method + return base.ReadByte(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count > BUFFER_SIZE) + { + // Trying to read more data than the buffer can hold + int alreadyCopied = 0; + if (_currentlyBufferedByteCount > 0) + { + Array.Copy(_buffer, _currentIndexInBuffer, buffer, offset, _currentlyBufferedByteCount); + alreadyCopied = _currentlyBufferedByteCount; + _currentIndexInBuffer = 0; + _currentlyBufferedByteCount = 0; + } + + int innerReadCount = _innerStream.Read(buffer, offset + alreadyCopied, count - alreadyCopied); + return innerReadCount + alreadyCopied; + } + else if (count <= _currentlyBufferedByteCount) + { + // Enough data buffered to satisfy read request + Array.Copy(_buffer, _currentIndexInBuffer, buffer, offset, count); + _currentIndexInBuffer += count; + _currentlyBufferedByteCount -= count; + return count; + } + else + { + // Need to read more data + int alreadyCopied = 0; + if (_currentlyBufferedByteCount > 0) + { + Array.Copy(_buffer, _currentIndexInBuffer, buffer, offset, _currentlyBufferedByteCount); + alreadyCopied = _currentlyBufferedByteCount; + _currentIndexInBuffer = 0; + _currentlyBufferedByteCount = 0; + } + + int innerReadCount = _innerStream.Read(_buffer, 0, BUFFER_SIZE); + _currentIndexInBuffer = 0; + _currentlyBufferedByteCount = innerReadCount; + + int remainingCopyCount = alreadyCopied + innerReadCount >= count + ? count - alreadyCopied + : innerReadCount; + + Array.Copy(_buffer, 0, buffer, offset + alreadyCopied, remainingCopyCount); + _currentIndexInBuffer += remainingCopyCount; + _currentlyBufferedByteCount -= remainingCopyCount; + + return alreadyCopied + remainingCopyCount; + } + } + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => _innerStream.Write(buffer, offset, count); +} diff --git a/src/MSBuildTaskHost/BackEnd/INodeEndpoint.cs b/src/MSBuildTaskHost/BackEnd/INodeEndpoint.cs new file mode 100644 index 00000000000..a3c8528d94e --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/INodeEndpoint.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Used to receive link status updates from an endpoint. +/// +/// The endpoint invoking the delegate. +/// The current status of the link. +internal delegate void LinkStatusChangedDelegate(INodeEndpoint endpoint, LinkStatus status); + +/// +/// The connection status of a link between the NodeEndpoint on the host and the NodeEndpoint +/// on the peer. +/// +internal enum LinkStatus +{ + /// + /// The connection has never been started. + /// + Inactive, + + /// + /// The connection is active, the most recent data has been successfully sent, and the + /// node is responding to pings. + /// + Active, + + /// + /// The connection has failed and been terminated. + /// + Failed, + + /// + /// The connection could not be made/timed out. + /// + ConnectionFailed, +} + +/// +/// This interface represents one end of a connection between the INodeProvider and a Node. +/// Implementations of this interface define the actual mechanism by which data is communicated. +/// +internal interface INodeEndpoint +{ + /// + /// Raised when the status of the node's link has changed. + /// + event LinkStatusChangedDelegate OnLinkStatusChanged; + + /// + /// Gets the current link status for this endpoint. + /// + LinkStatus LinkStatus { get; } + + /// + /// Waits for the remote node to establish a connection. + /// + /// The factory used to deserialize packets. + /// Only one of Listen() or Connect() may be called on an endpoint. + void Listen(INodePacketFactory factory); + + /// + /// Instructs the node to connect to its peer endpoint. + /// + /// The factory used to deserialize packets. + void Connect(INodePacketFactory factory); + + /// + /// Instructs the node to disconnect from its peer endpoint. + /// + void Disconnect(); + + /// + /// Sends a data packet to the node. + /// + /// The packet to be sent. + void SendData(INodePacket packet); + + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// This is needed to handle race condition when both client and server is gracefully about to close connection. + /// + void ClientWillDisconnect(); +} diff --git a/src/MSBuildTaskHost/BackEnd/INodePacket.cs b/src/MSBuildTaskHost/BackEnd/INodePacket.cs new file mode 100644 index 00000000000..348f8c08388 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/INodePacket.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Enumeration of all of the packet types used for communication. +/// Uses lower 6 bits for packet type (0-63), upper 2 bits reserved for flags. +/// +/// +/// This is a reduced set of packet types used by MSBuildTaskHost. +/// It is derived from the full set of packet types used for MSBuild's internal node communication. +/// +internal enum NodePacketType : byte +{ + // Mask for extracting packet type (lower 6 bits) + TypeMask = 0x3F, // 00111111 + + /// + /// A logging message. + /// + /// Contents: + /// Build Event Type + /// Build Event Args + /// + LogMessage = 0x08, + + /// + /// Informs the node that the build is complete. + /// + /// Contents: + /// Prepare For Reuse + /// + NodeBuildComplete = 0x09, + + /// + /// Reported by the node (or node provider) when a node has terminated. This is the final packet that will be received + /// from a node. + /// + /// Contents: + /// Reason + /// + NodeShutdown = 0x0A, + + /// + /// Notifies the task host to set the task-specific configuration for a particular task execution. + /// This is sent in place of NodeConfiguration and gives the task host all the information it needs + /// to set itself up and execute the task that matches this particular configuration. + /// + /// Contains: + /// Node ID (of parent MSBuild node, to make the logging work out) + /// Startup directory + /// Environment variables + /// UI Culture information + /// App Domain Configuration XML + /// Task name + /// Task assembly location + /// Parameter names and values to set to the task prior to execution + /// + TaskHostConfiguration = 0x0B, + + /// + /// Informs the parent node that the task host has finished executing a + /// particular task. Does not need to contain identifying information + /// about the task, because the task host will only ever be connected to + /// one parent node at a a time, and will only ever be executing one task + /// for that node at any one time. + /// + /// Contents: + /// Task result (success / failure) + /// Resultant parameter values (for output gathering) + /// + TaskHostTaskComplete = 0x0C, + + /// + /// Message sent from the node to its paired task host when a task that + /// supports ICancellableTask is cancelled. + /// + /// Contents: + /// (nothing) + /// + TaskHostTaskCancelled = 0x0D, +} + +/// +/// This interface represents a packet which may be transmitted using an INodeEndpoint. +/// Implementations define the serialized form of the data. +/// +internal interface INodePacket : ITranslatable +{ + /// + /// Gets the type of the packet. Used to reconstitute the packet using the correct factory. + /// + NodePacketType Type { get; } +} + +/// +/// Provides utilities for handling node packet types and extended headers in MSBuild's distributed build system. +/// +/// This class manages the communication protocol between build nodes, including: +/// - Packet versioning for protocol compatibility +/// - Extended header flags for enhanced packet metadata +/// - Type extraction and manipulation for network communication +/// +/// The packet format uses the upper 2 bits (6-7) for flags while preserving +/// the lower 6 bits for the actual packet type enumeration. +/// +internal static class NodePacketTypeExtensions +{ + /// + /// Defines the communication protocol version for node communication. + /// + /// Version 1: Introduced for the .NET Task Host protocol. This version + /// excludes the translation of appDomainConfig within TaskHostConfiguration + /// to maintain backward compatibility and reduce serialization overhead. + /// + /// Version 2: Adds support of HostServices and target name translation in TaskHostConfiguration. + /// + /// When incrementing this version, ensure compatibility with existing + /// task hosts and update the corresponding deserialization logic. + /// + public const byte PacketVersion = 2; + + // Flag bits in upper 2 bits + private const byte ExtendedHeaderFlag = 0x40; // Bit 6: 01000000 + + /// + /// Determines if a packet has an extended header by checking if the extended header flag is set. + /// Uses bit 6 which is now safely separated from packet type values. + /// + /// The raw packet type byte. + /// True if the packet has an extended header; otherwise, false. + public static bool HasExtendedHeader(byte rawType) + => (rawType & ExtendedHeaderFlag) != 0; + + /// + /// Get base packet type, stripping all flag bits (bits 6 and 7). + /// + /// The raw packet type byte with potential flags. + /// The clean packet type without flag bits. + public static NodePacketType GetNodePacketType(byte rawType) + => (NodePacketType)(rawType & (byte)NodePacketType.TypeMask); + + /// + /// Reads the protocol version from an extended header in the stream. + /// This method expects the stream to be positioned at the version byte. + /// + /// The stream to read the version byte from. + /// The protocol version byte read from the stream. + /// Thrown when the stream ends unexpectedly while reading the version. + public static byte ReadVersion(Stream stream) + { + int value = stream.ReadByte(); + if (value == -1) + { + throw new EndOfStreamException("Unexpected end of stream while reading version"); + } + + return (byte)value; + } + + /// + /// Negotiates the packet version to use for communication between nodes. + /// Returns the lower of the two versions to ensure compatibility between + /// nodes that may be running different versions of MSBuild. + /// + /// This allows forward and backward compatibility when nodes with different + /// packet versions communicate - they will use the lowest common version + /// that both understand. + /// + /// The packet version supported by the other node. + /// The negotiated protocol version that both nodes can use (the minimum of the two versions). + public static byte GetNegotiatedPacketVersion(byte otherPacketVersion) + => Math.Min(PacketVersion, otherPacketVersion); +} diff --git a/src/MSBuildTaskHost/BackEnd/INodePacketFactory.cs b/src/MSBuildTaskHost/BackEnd/INodePacketFactory.cs new file mode 100644 index 00000000000..a115dddc453 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/INodePacketFactory.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// A delegate representing factory methods used to re-create packets deserialized from a stream. +/// +/// The translator containing the packet data. +/// The packet reconstructed from the stream. +internal delegate INodePacket NodePacketFactoryMethod(ITranslator translator); + +/// +/// This interface represents an object which is used to reconstruct packet objects from +/// binary data. +/// +internal interface INodePacketFactory +{ + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler); + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + void UnregisterPacketHandler(NodePacketType packetType); + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator); + + /// + /// Takes a serializer and deserializes the packet. + /// + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator); + + /// + /// Routes the specified packet. + /// + /// The node from which the packet was received. + /// The packet to route. + void RoutePacket(int nodeId, INodePacket packet); +} diff --git a/src/MSBuildTaskHost/BackEnd/INodePacketHandler.cs b/src/MSBuildTaskHost/BackEnd/INodePacketHandler.cs new file mode 100644 index 00000000000..acdf0d48205 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/INodePacketHandler.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Objects which wish to receive packets from the NodePacketRouter must implement this interface. +/// +internal interface INodePacketHandler +{ + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + void PacketReceived(int node, INodePacket packet); +} diff --git a/src/MSBuildTaskHost/BackEnd/ITranslatable.cs b/src/MSBuildTaskHost/BackEnd/ITranslatable.cs new file mode 100644 index 00000000000..455728022d2 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/ITranslatable.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// An interface representing an object which may be serialized by the node packet serializer. +/// +internal interface ITranslatable +{ + /// + /// Reads or writes the packet to the serializer. + /// + void Translate(ITranslator translator); +} diff --git a/src/MSBuildTaskHost/BackEnd/ITranslator.cs b/src/MSBuildTaskHost/BackEnd/ITranslator.cs new file mode 100644 index 00000000000..f5473d38d8c --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/ITranslator.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// This delegate is used for objects which do not have public parameterless constructors and must be constructed using +/// another method. When invoked, this delegate should return a new object which has been translated appropriately. +/// +/// The type to be translated. +internal delegate T NodePacketValueFactory(ITranslator translator); + +/// +/// Delegate for users that want to translate an arbitrary structure that doesn't implement (e.g. translating a complex collection) +/// +/// the translator +/// the object to translate +internal delegate void ObjectTranslator(ITranslator translator, ref T objectToTranslate); + +/// +/// Delegate for users that want to translate an arbitrary structure that doesn't implement (e.g. translating a complex collection) +/// +/// the translator +/// The factory to use to create the value. +/// the object to translate +internal delegate void ObjectTranslatorWithValueFactory(ITranslator translator, NodePacketValueFactory valueFactory, ref T objectToTranslate); + +/// +/// This delegate is used to create arbitrary collection types for serialization. +/// +/// The type of dictionary to be created. +internal delegate T NodePacketCollectionCreator(int capacity); + +/// +/// The serialization mode. +/// +internal enum TranslationDirection +{ + /// + /// Indicates the serializer is operating in write mode. + /// + WriteToStream, + + /// + /// Indicates the serializer is operating in read mode. + /// + ReadFromStream +} + +/// +/// This interface represents an object which aids objects in serializing and +/// deserializing INodePackets. +/// +/// +/// The reason we bother with a custom serialization mechanism at all is two fold: +/// 1. The .Net serialization mechanism is inefficient, even if you implement ISerializable +/// with your own custom mechanism. This is because the serializer uses a bag called +/// SerializationInfo into which you are expected to drop all your data. This adds +/// an unnecessary level of indirection to the serialization routines and prevents direct, +/// efficient access to the byte-stream. +/// 2. You have to implement both a reader and writer part, which introduces the potential for +/// error should the classes be later modified. If the reader and writer methods are not +/// kept in perfect sync, serialization errors will occur. Our custom serializer eliminates +/// that by ensuring a single Translate method on a given object can handle both reads and +/// writes without referencing any field more than once. +/// +internal interface ITranslator : IDisposable +{ + /// + /// Gets or sets the negotiated packet version between the communicating nodes. + /// This represents the minimum packet version supported by both the sender and receiver, + /// ensuring backward compatibility during cross-version communication. + /// + /// + /// This version is determined during the initial handshake between nodes and may differ + /// from NodePacketTypeExtensions.PacketVersion when nodes are running different MSBuild versions. + /// The negotiated version is used to conditionally serialize/deserialize fields that may + /// not be supported by older packet versions. + /// + byte NegotiatedPacketVersion { get; set; } + + /// + /// Gets the current serialization mode. + /// + TranslationDirection Mode { get; } + + /// + /// Gets the binary reader. + /// + /// + /// This should ONLY be used when absolutely necessary for translation. It is generally unnecessary for the + /// translating object to know the direction of translation. Use one of the Translate methods instead. + /// + BinaryReader Reader { get; } + + /// + /// Gets the binary writer. + /// + /// + /// This should ONLY be used when absolutely necessary for translation. It is generally unnecessary for the + /// translating object to know the direction of translation. Use one of the Translate methods instead. + /// + BinaryWriter Writer { get; } + + /// + /// Translates a boolean. + /// + /// The value to be translated. + void Translate(ref bool value); + + /// + /// Translates an array. + /// + /// The array to be translated. + void Translate(ref bool[]? array); + + /// + /// Translates a byte. + /// + /// The value to be translated. + void Translate(ref byte value); + + /// + /// Translates a short. + /// + /// The value to be translated. + void Translate(ref short value); + + /// + /// Translates a unsigned short. + /// + /// The value to be translated. + void Translate(ref ushort value); + + /// + /// Translates an integer. + /// + /// The value to be translated. + void Translate(ref int value); + + /// + /// Translates an array. + /// + /// The array to be translated. + void Translate(ref int[]? array); + + /// + /// Translates a long. + /// + /// The value to be translated. + void Translate(ref long value); + + /// + /// Translates a string. + /// + /// The value to be translated. + void Translate(ref string? value); + + /// + /// Translates a double. + /// + /// The value to be translated. + void Translate(ref double value); + + /// + /// Translates a string array. + /// + /// The array to be translated. + void Translate(ref string[]? array); + + /// + /// Translates a collection of T into the specified type using an and . + /// + /// The collection to be translated. + /// The translator to use for the values in the collection. + /// The factory to create the ICollection. + /// The type contained in the collection. + /// The type of collection to be created. + void Translate( + ref ICollection? collection, + ObjectTranslator objectTranslator, + NodePacketCollectionCreator collectionFactory) + where L : ICollection; + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + void Translate(ref DateTime value); + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum; + + void TranslateException(ref Exception? value); + + /// + /// Translates a culture. + /// + /// The culture. + void TranslateCulture(ref CultureInfo? culture); + + /// + /// Translates a byte array. + /// + /// The array to be translated. + void Translate(ref byte[]? byteArray); + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + void TranslateDictionary(ref Dictionary? dictionary, IEqualityComparer comparer); + + /// + /// Translates a dictionary of { string, T }. + /// + /// The reference type for the values, which implements INodePacketTranslatable. + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + /// The translator to use for the values in the dictionary. + /// /// The factory to use to create the value. + void TranslateDictionary( + ref Dictionary? dictionary, + IEqualityComparer comparer, + ObjectTranslatorWithValueFactory objectTranslator, + NodePacketValueFactory valueFactory) + where T : class; + + /// + /// Translates the boolean that says whether this value is null or not. + /// + /// The object to test. + /// The type of object to test. + /// True if the object should be written, false otherwise. + bool TranslateNullable(T? value) + where T : class; +} diff --git a/src/MSBuildTaskHost/BackEnd/InterningBinaryReader.cs b/src/MSBuildTaskHost/BackEnd/InterningBinaryReader.cs new file mode 100644 index 00000000000..96356caf896 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/InterningBinaryReader.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using Microsoft.Build.TaskHost.Utilities; +using Microsoft.NET.StringTools; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Replacement for BinaryReader which attempts to intern the strings read by ReadString. +/// +internal sealed class InterningBinaryReader : BinaryReader +{ + /// + /// The maximum size, in bytes, to read at once. + /// +#if DEBUG + private const int MaxCharsBuffer = 10; +#else + private const int MaxCharsBuffer = 20000; +#endif + + /// + /// Shared buffer saves allocating these arrays many times. + /// + private readonly Buffer _buffer; + + /// + /// The decoder used to translate from UTF8 (or whatever). + /// + private readonly Decoder _decoder; + + private InterningBinaryReader(Stream input, Buffer buffer) + : base(input, Encoding.UTF8) + { + if (input == null) + { + throw new InvalidOperationException(); + } + + _buffer = buffer; + _decoder = Encoding.UTF8.GetDecoder(); + } + + /// + /// Read a string while checking the string precursor for intern opportunities. + /// Taken from ndp\clr\src\bcl\system\io\binaryreader.cs-ReadString(). + /// + public override string ReadString() + { + char[]? resultBuffer = null; + try + { + int currPos = 0; + int n = 0; + int stringLength; + int readLength; + int charsRead = 0; + + // Length of the string in bytes, not chars + stringLength = Read7BitEncodedInt(); + if (stringLength < 0) + { + throw new IOException(); + } + + if (stringLength == 0) + { + return string.Empty; + } + + char[] charBuffer = _buffer.CharBuffer; + do + { + readLength = ((stringLength - currPos) > MaxCharsBuffer) ? MaxCharsBuffer : (stringLength - currPos); + + byte[]? rawBuffer = null; + int rawPosition = 0; + + if (BaseStream is MemoryStream memoryStream) + { + // Optimization: we can avoid reading into a byte buffer + // and instead read directly from the memorystream's backing buffer + rawBuffer = memoryStream.GetBuffer(); + rawPosition = (int)memoryStream.Position; + int length = (int)memoryStream.Length; + n = (rawPosition + readLength) < length ? readLength : length - rawPosition; + + // Attempt to track down an intermittent failure -- n should not ever be negative, but + // we're occasionally seeing it when we do the decoder.GetChars below -- by providing + // a bit more information when we do hit the error, in the place where (by code inspection) + // the actual error seems most likely to be occurring. + if (n < 0) + { + ErrorUtilities.ThrowInternalError($"From calculating based on the memorystream, about to read n = {n}. length = {length}, rawPosition = {rawPosition}, readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}."); + } + + memoryStream.Seek(n, SeekOrigin.Current); + } + + if (rawBuffer == null) + { + rawBuffer = _buffer.ByteBuffer; + rawPosition = 0; + n = BaseStream.Read(rawBuffer, 0, readLength); + + // See above explanation -- the OutOfRange exception may also be coming from our setting of n here ... + if (n < 0) + { + ErrorUtilities.ThrowInternalError($"From getting the length out of BaseStream.Read directly, about to read n = {n}. readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}"); + } + } + + if (n == 0) + { + throw new EndOfStreamException(); + } + + if (currPos == 0 && n == stringLength) + { + charsRead = _decoder.GetChars(rawBuffer, rawPosition, n, charBuffer, 0); + return Strings.WeakIntern(charBuffer.AsSpan(0, charsRead)); + } + + resultBuffer ??= new char[stringLength]; // Actual string length in chars may be smaller. + charsRead += _decoder.GetChars(rawBuffer, rawPosition, n, resultBuffer, charsRead); + + currPos += n; + } + while (currPos < stringLength); + + return Strings.WeakIntern(resultBuffer.AsSpan(0, charsRead)); + } + catch (Exception e) + { + Debug.Fail(e.ToString()); + throw; + } + } + + /// + /// A shared buffer to avoid extra allocations in InterningBinaryReader. + /// + /// + /// The caller is responsible for managing the lifetime of the returned buffer and for passing it to . + /// + internal static BinaryReaderFactory CreateSharedBuffer() + => new Buffer(); + + /// + /// Holds the preallocated buffer. + /// + private sealed class Buffer : BinaryReaderFactory + { + /// + /// Gets the char buffer. + /// + internal char[] CharBuffer + => field ??= new char[MaxCharsBuffer]; + + /// + /// Gets the byte buffer. + /// + internal byte[] ByteBuffer + => field ??= new byte[Encoding.UTF8.GetMaxByteCount(MaxCharsBuffer)]; + + public override BinaryReader Create(Stream stream) + => new InterningBinaryReader(stream, buffer: this); + } +} diff --git a/src/MSBuildTaskHost/BackEnd/LogMessagePacketBase.cs b/src/MSBuildTaskHost/BackEnd/LogMessagePacketBase.cs new file mode 100644 index 00000000000..195bf6fe771 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/LogMessagePacketBase.cs @@ -0,0 +1,662 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.Exceptions; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +// On .NET Framework 3.5, Microsoft.Build.Framework includes the following concrete event args types: +// +// - BuildErrorEventArgs +// - BuildFinishedEventArgs +// - BuildMessageEventArgs +// - BuildStartedEventArgs +// - BuildWarningEventArgs +// - ExternalProjectFinishedEventArgs +// - ExternalProjectStartedEventArgs +// - ProjectFinishedEventArgs +// - ProjectStartedEventArgs +// - TargetFinishedEventArgs +// - TargetStartedEventArgs +// - TaskCommandLineEventArgs +// - TaskFinishedEventArgs +// - TaskStartedEventArgs + +/// +/// An enumeration of all the types of BuildEventArgs that can be +/// packaged by this logMessagePacket +/// +internal enum LoggingEventType : int +{ + /// + /// An invalid eventId, used during initialization of a . + /// + Invalid = -1, + + /// + /// Event is a CustomEventArgs. + /// + CustomEvent = 0, + + /// + /// Event is a . + /// + BuildErrorEvent = 1, + + /// + /// Event is a . + /// + BuildFinishedEvent = 2, + + /// + /// Event is a . + /// + BuildMessageEvent = 3, + + /// + /// Event is a . + /// + BuildStartedEvent = 4, + + /// + /// Event is a . + /// + BuildWarningEvent = 5, + + /// + /// Event is a . + /// + ProjectFinishedEvent = 6, + + /// + /// Event is a . + /// + ProjectStartedEvent = 7, + + /// + /// Event is a . + /// + TargetStartedEvent = 8, + + /// + /// Event is a . + /// + TargetFinishedEvent = 9, + + /// + /// Event is a . + /// + TaskStartedEvent = 10, + + /// + /// Event is a . + /// + TaskFinishedEvent = 11, + + /// + /// Event is a . + /// + TaskCommandLineEvent = 12, + + /// + /// Event is . + /// + ExternalProjectStartedEvent = 22, + + /// + /// Event is . + /// + ExternalProjectFinishedEvent = 23, +} + +/// +/// A packet to encapsulate a BuildEventArg logging message. +/// Contents: +/// Build Event Type +/// Build Event Args +/// +internal sealed class LogMessagePacketBase : INodePacket +{ + private const string WriteToStreamMethodName = "WriteToStream"; + private const string CreateFromStreamMethodName = "CreateFromStream"; + + private const BindingFlags NonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance; + + /// + /// The packet version, which is based on the CLR version. Cached because querying Environment.Version each time becomes an allocation bottleneck. + /// + private static readonly int s_defaultPacketVersion = (Environment.Version.Major * 10) + Environment.Version.Minor; + + /// + /// Dictionary of methods used to read BuildEventArgs. + /// + private static readonly Dictionary s_readMethodCache = []; + + /// + /// Dictionary of methods used to write BuildEventArgs. + /// + private static readonly Dictionary s_writeMethodCache = []; + + /// + /// The event type of the buildEventArg based on the + /// LoggingEventType enumeration + /// + private LoggingEventType _eventType = LoggingEventType.Invalid; + + /// + /// The buildEventArg which is encapsulated by the packet. + /// + private BuildEventArgs? _buildEvent; + + /// + /// The sink id + /// + private int _sinkId; + + /// + /// Encapsulates the buildEventArg in this packet. + /// + public LogMessagePacketBase(KeyValuePair? nodeBuildEvent) + { + ErrorUtilities.VerifyThrow(nodeBuildEvent != null, "nodeBuildEvent was null"); + _buildEvent = nodeBuildEvent.Value.Value; + _sinkId = nodeBuildEvent.Value.Key; + _eventType = GetLoggingEventId(_buildEvent); + } + + /// + /// Delegate representing a method on the BuildEventArgs classes used to write to a stream. + /// + private delegate void WriteToStreamMethod(BinaryWriter writer); + + /// + /// Delegate representing a method on the BuildEventArgs classes used to read from a stream. + /// + private delegate void CreateFromStreamMethod(BinaryReader reader); + + /// + /// Gets the nodePacket Type, in this case the packet is a Logging Message. + /// + public NodePacketType Type => NodePacketType.LogMessage; + + /// + /// Reads/writes this packet. + /// + public void Translate(ITranslator translator) + { + translator.TranslateEnum(ref _eventType, (int)_eventType); + translator.Translate(ref _sinkId); + if (translator.Mode == TranslationDirection.ReadFromStream) + { + ReadFromStream(translator); + } + else + { + WriteToStream(translator); + } + } + + /// + /// Writes the logging packet to the translator. + /// + private void WriteToStream(ITranslator translator) + { + ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event"); + + MethodInfo? methodInfo = null; + lock (s_writeMethodCache) + { + if (!s_writeMethodCache.TryGetValue(_eventType, out methodInfo)) + { + Type eventDerivedType = _buildEvent!.GetType(); + methodInfo = eventDerivedType.GetMethod(WriteToStreamMethodName, NonPublicInstance); + s_writeMethodCache.Add(_eventType, methodInfo); + } + } + + int packetVersion = s_defaultPacketVersion; + + // Make sure the other side knows what sort of serialization is coming + translator.Translate(ref packetVersion); + + // Note: This should always be true in MSBuildTaskHost (and methodInfo should never be null). + // The .NET 3.5 event args have correct "WriteToStream" methods. + bool eventCanSerializeItself = methodInfo != null; + + translator.Translate(ref eventCanSerializeItself); + + if (eventCanSerializeItself) + { + WriteToStreamMethod writerMethod = CreateDelegateRobust(_buildEvent!, methodInfo!); + writerMethod(translator.Writer); + } + else + { + WriteEventToStream(_buildEvent!, _eventType, translator); + } + } + + /// + /// Reads the logging packet from the translator. + /// + private void ReadFromStream(ITranslator translator) + { + ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event"); + + _buildEvent = GetBuildEventArgFromId(); + + // The other side is telling us whether the event knows how to log itself, or whether we're going to have + // to do it manually + int packetVersion = s_defaultPacketVersion; + translator.Translate(ref packetVersion); + + bool eventCanSerializeItself = true; + translator.Translate(ref eventCanSerializeItself); + + // Note: This should always be true in MSBuildTaskHost (and methodInfo should never be null). + // The .NET 3.5 event args have correct "CreateFromStream" methods. + if (eventCanSerializeItself) + { + MethodInfo? methodInfo = null; + lock (s_readMethodCache) + { + if (!s_readMethodCache.TryGetValue(_eventType, out methodInfo)) + { + Type eventDerivedType = _buildEvent.GetType(); + methodInfo = eventDerivedType.GetMethod(CreateFromStreamMethodName, NonPublicInstance); + s_readMethodCache.Add(_eventType, methodInfo); + } + } + + CreateFromStreamMethod readerMethod = CreateDelegateRobust(_buildEvent, methodInfo); + + readerMethod(translator.Reader); + } + else + { + _buildEvent = ReadEventFromStream(_eventType, translator); + ErrorUtilities.VerifyThrow(_buildEvent is not null, $"Unsupported LoggingEventType {_eventType}"); + } + + _eventType = GetLoggingEventId(_buildEvent); + } + + /// + /// Wrapper for Delegate.CreateDelegate with retries. + /// + private static T CreateDelegateRobust(object firstArgument, MethodInfo methodInfo) + where T : class, Delegate + { + Type type = typeof(T); + + for (int i = 0; i < 5; i++) + { + try + { + return (T)Delegate.CreateDelegate(type, firstArgument, methodInfo); + } + catch (FileLoadException) + { + // Sometimes, in 64-bit processes, the fusion load of Microsoft.Build.Framework.dll + // spontaneously fails when trying to bind to the delegate. However, it seems to + // not repeat on additional tries -- so we'll try again a few times. However, if + // it keeps happening, it's probably a real problem, so we want to go ahead and + // throw to let the user know what's up. + } + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// Takes in a id (LoggingEventType as an int) and creates the correct specific logging class + /// + private BuildEventArgs GetBuildEventArgFromId() => _eventType switch + { + LoggingEventType.BuildErrorEvent => new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null), + LoggingEventType.BuildFinishedEvent => new BuildFinishedEventArgs(null, null, false), + LoggingEventType.BuildMessageEvent => new BuildMessageEventArgs(null, null, null, MessageImportance.Normal), + LoggingEventType.BuildStartedEvent => new BuildStartedEventArgs(null, null), + LoggingEventType.BuildWarningEvent => new BuildWarningEventArgs(null, null, null, -1, -1, -1, -1, null, null, null), + LoggingEventType.ProjectFinishedEvent => new ProjectFinishedEventArgs(null, null, null, false), + LoggingEventType.ProjectStartedEvent => new ProjectStartedEventArgs(null, null, null, null, null, null), + LoggingEventType.TargetStartedEvent => new TargetStartedEventArgs(null, null, null, null, null), + LoggingEventType.TargetFinishedEvent => new TargetFinishedEventArgs(null, null, null, null, null, false), + LoggingEventType.TaskStartedEvent => new TaskStartedEventArgs(null, null, null, null, null), + LoggingEventType.TaskFinishedEvent => new TaskFinishedEventArgs(null, null, null, null, null, false), + LoggingEventType.TaskCommandLineEvent => new TaskCommandLineEventArgs(null, null, MessageImportance.Normal), + LoggingEventType.ExternalProjectStartedEvent => new ExternalProjectStartedEventArgs(null, null, null, null, null), + LoggingEventType.ExternalProjectFinishedEvent => new ExternalProjectFinishedEventArgs(null, null, null, null, false), + + _ => throw new InternalErrorException($"Should not get to the default of GetBuildEventArgFromId ID: {_eventType}") + }; + + /// + /// Based on the type of the BuildEventArg to be wrapped + /// generate an Id which identifies which concrete type the + /// BuildEventArg is. + /// + /// Argument to get the type Id for + /// An enumeration entry which represents the type + private LoggingEventType GetLoggingEventId(BuildEventArgs eventArg) + { + Type eventType = eventArg.GetType(); + if (eventType == typeof(BuildMessageEventArgs)) + { + return LoggingEventType.BuildMessageEvent; + } + else if (eventType == typeof(TaskCommandLineEventArgs)) + { + return LoggingEventType.TaskCommandLineEvent; + } + else if (eventType == typeof(ProjectFinishedEventArgs)) + { + return LoggingEventType.ProjectFinishedEvent; + } + else if (eventType == typeof(ProjectStartedEventArgs)) + { + return LoggingEventType.ProjectStartedEvent; + } + else if (eventType == typeof(ExternalProjectStartedEventArgs)) + { + return LoggingEventType.ExternalProjectStartedEvent; + } + else if (eventType == typeof(ExternalProjectFinishedEventArgs)) + { + return LoggingEventType.ExternalProjectFinishedEvent; + } + else if (eventType == typeof(TargetStartedEventArgs)) + { + return LoggingEventType.TargetStartedEvent; + } + else if (eventType == typeof(TargetFinishedEventArgs)) + { + return LoggingEventType.TargetFinishedEvent; + } + else if (eventType == typeof(TaskStartedEventArgs)) + { + return LoggingEventType.TaskStartedEvent; + } + else if (eventType == typeof(TaskFinishedEventArgs)) + { + return LoggingEventType.TaskFinishedEvent; + } + else if (eventType == typeof(BuildFinishedEventArgs)) + { + return LoggingEventType.BuildFinishedEvent; + } + else if (eventType == typeof(BuildStartedEventArgs)) + { + return LoggingEventType.BuildStartedEvent; + } + else if (eventType == typeof(BuildWarningEventArgs)) + { + return LoggingEventType.BuildWarningEvent; + } + else if (eventType == typeof(BuildErrorEventArgs)) + { + return LoggingEventType.BuildErrorEvent; + } + else + { + return LoggingEventType.CustomEvent; + } + } + + /// + /// Given a build event that is presumed to be 2.0 (due to its lack of a "WriteToStream" method) and its + /// LoggingEventType, serialize that event to the stream. + /// + /// + /// Override to customize serialization per-assembly without relying on compile directives. + /// + private void WriteEventToStream(BuildEventArgs buildEvent, LoggingEventType eventType, ITranslator translator) + { + string? message = buildEvent.Message; + string? helpKeyword = buildEvent.HelpKeyword; + string? senderName = buildEvent.SenderName; + + translator.Translate(ref message); + translator.Translate(ref helpKeyword); + translator.Translate(ref senderName); + + // It is essential that you translate in the same order during writing and reading + switch (eventType) + { + case LoggingEventType.BuildMessageEvent: + WriteBuildMessageEventToStream((BuildMessageEventArgs)buildEvent, translator); + break; + case LoggingEventType.TaskCommandLineEvent: + WriteTaskCommandLineEventToStream((TaskCommandLineEventArgs)buildEvent, translator); + break; + case LoggingEventType.BuildErrorEvent: + WriteBuildErrorEventToStream((BuildErrorEventArgs)buildEvent, translator); + break; + case LoggingEventType.BuildWarningEvent: + WriteBuildWarningEventToStream((BuildWarningEventArgs)buildEvent, translator); + break; + default: + ErrorUtilities.ThrowInternalError($"Not Supported LoggingEventType {eventType}"); + break; + } + } + + /// + /// Write Build Warning Log message into the translator + /// + private void WriteBuildWarningEventToStream(BuildWarningEventArgs buildWarningEventArgs, ITranslator translator) + { + string? code = buildWarningEventArgs.Code; + translator.Translate(ref code); + + int columnNumber = buildWarningEventArgs.ColumnNumber; + translator.Translate(ref columnNumber); + + int endColumnNumber = buildWarningEventArgs.EndColumnNumber; + translator.Translate(ref endColumnNumber); + + int endLineNumber = buildWarningEventArgs.EndLineNumber; + translator.Translate(ref endLineNumber); + + string? file = buildWarningEventArgs.File; + translator.Translate(ref file); + + int lineNumber = buildWarningEventArgs.LineNumber; + translator.Translate(ref lineNumber); + + string? subCategory = buildWarningEventArgs.Subcategory; + translator.Translate(ref subCategory); + } + + /// + /// Write a Build Error message into the translator + /// + private void WriteBuildErrorEventToStream(BuildErrorEventArgs buildErrorEventArgs, ITranslator translator) + { + string? code = buildErrorEventArgs.Code; + translator.Translate(ref code); + + int columnNumber = buildErrorEventArgs.ColumnNumber; + translator.Translate(ref columnNumber); + + int endColumnNumber = buildErrorEventArgs.EndColumnNumber; + translator.Translate(ref endColumnNumber); + + int endLineNumber = buildErrorEventArgs.EndLineNumber; + translator.Translate(ref endLineNumber); + + string? file = buildErrorEventArgs.File; + translator.Translate(ref file); + + int lineNumber = buildErrorEventArgs.LineNumber; + translator.Translate(ref lineNumber); + + string? subCategory = buildErrorEventArgs.Subcategory; + translator.Translate(ref subCategory); + } + + /// + /// Write Task Command Line log message into the translator. + /// + private void WriteTaskCommandLineEventToStream(TaskCommandLineEventArgs taskCommandLineEventArgs, ITranslator translator) + { + MessageImportance importance = taskCommandLineEventArgs.Importance; + translator.TranslateEnum(ref importance, (int)importance); + + string? commandLine = taskCommandLineEventArgs.CommandLine; + translator.Translate(ref commandLine); + + string? taskName = taskCommandLineEventArgs.TaskName; + translator.Translate(ref taskName); + } + + /// + /// Write a "standard" Message Log the translator + /// + private void WriteBuildMessageEventToStream(BuildMessageEventArgs buildMessageEventArgs, ITranslator translator) + { + MessageImportance importance = buildMessageEventArgs.Importance; + translator.TranslateEnum(ref importance, (int)importance); + } + + /// + /// Given a build event that is presumed to be 2.0 (due to its lack of a "ReadFromStream" method) and its + /// LoggingEventType, read that event from the stream. + /// + /// + /// Override to customize serialization per-assembly without relying on compile directives. + /// + private BuildEventArgs? ReadEventFromStream(LoggingEventType eventType, ITranslator translator) + { + string? message = null; + string? helpKeyword = null; + string? senderName = null; + + translator.Translate(ref message); + translator.Translate(ref helpKeyword); + translator.Translate(ref senderName); + + return eventType switch + { + LoggingEventType.TaskCommandLineEvent => ReadTaskCommandLineEventFromStream(translator), + LoggingEventType.BuildErrorEvent => ReadTaskBuildErrorEventFromStream(translator, message, helpKeyword, senderName), + LoggingEventType.BuildMessageEvent => ReadBuildMessageEventFromStream(translator, message, helpKeyword, senderName), + LoggingEventType.BuildWarningEvent => ReadBuildWarningEventFromStream(translator, message, helpKeyword, senderName), + _ => null, + }; + } + + /// + /// Read and reconstruct a BuildWarningEventArgs from the stream. + /// + private BuildWarningEventArgs ReadBuildWarningEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName) + { + string? code = null; + translator.Translate(ref code); + + int columnNumber = -1; + translator.Translate(ref columnNumber); + + int endColumnNumber = -1; + translator.Translate(ref endColumnNumber); + + int endLineNumber = -1; + translator.Translate(ref endLineNumber); + + string? file = null; + translator.Translate(ref file); + + int lineNumber = -1; + translator.Translate(ref lineNumber); + + string? subCategory = null; + translator.Translate(ref subCategory); + + return new BuildWarningEventArgs( + subCategory, + code, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + senderName); + } + + /// + /// Read and reconstruct a BuildErrorEventArgs from the stream. + /// + private BuildErrorEventArgs ReadTaskBuildErrorEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName) + { + string? code = null; + translator.Translate(ref code); + + int columnNumber = -1; + translator.Translate(ref columnNumber); + + int endColumnNumber = -1; + translator.Translate(ref endColumnNumber); + + int endLineNumber = -1; + translator.Translate(ref endLineNumber); + + string? file = null; + translator.Translate(ref file); + + int lineNumber = -1; + translator.Translate(ref lineNumber); + + string? subCategory = null; + translator.Translate(ref subCategory); + + return new BuildErrorEventArgs( + subCategory, + code, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + senderName); + } + + /// + /// Read and reconstruct a TaskCommandLineEventArgs from the stream. + /// + private TaskCommandLineEventArgs ReadTaskCommandLineEventFromStream(ITranslator translator) + { + MessageImportance importance = MessageImportance.Normal; + translator.TranslateEnum(ref importance, (int)importance); + + string? commandLine = null; + translator.Translate(ref commandLine); + + string? taskName = null; + translator.Translate(ref taskName); + + return new TaskCommandLineEventArgs(commandLine, taskName, importance); + } + + /// + /// Read and reconstruct a BuildMessageEventArgs from the stream. + /// + private BuildMessageEventArgs ReadBuildMessageEventFromStream(ITranslator translator, string? message, string? helpKeyword, string? senderName) + { + MessageImportance importance = MessageImportance.Normal; + + translator.TranslateEnum(ref importance, (int)importance); + + return new BuildMessageEventArgs(message, helpKeyword, senderName, importance); + } +} diff --git a/src/MSBuildTaskHost/BackEnd/NodeBuildComplete.cs b/src/MSBuildTaskHost/BackEnd/NodeBuildComplete.cs new file mode 100644 index 00000000000..725708126c8 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/NodeBuildComplete.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// The NodeBuildComplete packet is used to indicate to a node that it should clean up its current build and +/// possibly prepare for node reuse. +/// +internal sealed class NodeBuildComplete : INodePacket +{ + /// + /// Flag indicating if the node should prepare for reuse after cleanup. + /// + private bool _prepareForReuse; + + public NodeBuildComplete(bool prepareForReuse) + { + _prepareForReuse = prepareForReuse; + } + + private NodeBuildComplete() + { + } + + /// + /// Gets a value indicating whether the node should prepare for reuse. + /// + public bool PrepareForReuse => _prepareForReuse; + + /// + /// Gets the packet type. + /// + public NodePacketType Type => NodePacketType.NodeBuildComplete; + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + => translator.Translate(ref _prepareForReuse); + + internal static NodeBuildComplete FactoryForDeserialization(ITranslator translator) + { + var packet = new NodeBuildComplete(); + packet.Translate(translator); + return packet; + } +} diff --git a/src/MSBuildTaskHost/BackEnd/NodeEndpointOutOfProcTaskHost.cs b/src/MSBuildTaskHost/BackEnd/NodeEndpointOutOfProcTaskHost.cs new file mode 100644 index 00000000000..b16ed9f33e9 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/NodeEndpointOutOfProcTaskHost.cs @@ -0,0 +1,592 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; +using Microsoft.Build.TaskHost.Collections; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// This is an implementation of INodeEndpoint for the out-of-proc nodes. It acts only as a client. +/// +[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "It is expected to keep the stream open for the process lifetime")] +internal sealed class NodeEndpointOutOfProcTaskHost : INodeEndpoint +{ + /// + /// The size of the buffers to use for named pipes. + /// + private const int PipeBufferSize = 131072; + + /// + /// The pipe client used by the nodes. + /// + private readonly NamedPipeServerStream _pipeServer; + + /// + /// Per-node shared read buffer. + /// + private readonly BinaryReaderFactory _sharedReadBuffer; + + /// + /// A way to cache a byte array when writing out packets. + /// + private readonly MemoryStream _packetStream; + + /// + /// A binary writer to help write into . + /// + private readonly BinaryWriter _binaryWriter; + + /// + /// Represents the version of the parent packet associated with the node instantiation. + /// + private readonly byte _parentPacketVersion; + + /// + /// The current communication status of the node. + /// + private LinkStatus _status; + + /// + /// Set when a packet is available in the packet queue. + /// + private AutoResetEvent? _packetAvailable; + + /// + /// Set when the asynchronous packet pump should terminate. + /// + private AutoResetEvent? _terminatePacketPump; + + /// + /// True if this side is gracefully disconnecting. + /// In such case we have sent last packet to client side and we expect + /// client will soon broke pipe connection - unless server do it first. + /// + private bool _isClientDisconnecting; + + /// + /// The thread which runs the asynchronous packet pump. + /// + private Thread? _packetPump; + + /// + /// The factory used to create and route packets. + /// + private INodePacketFactory? _packetFactory; + + /// + /// The asynchronous packet queue. + /// + /// + /// Operations on this queue must be synchronized since it is accessible by multiple threads. + /// Use a lock on the packetQueue itself. + /// + private ConcurrentQueue? _packetQueue; + + public NodeEndpointOutOfProcTaskHost(byte parentPacketVersion) + { + _status = LinkStatus.Inactive; + _sharedReadBuffer = InterningBinaryReader.CreateSharedBuffer(); + + _packetStream = new MemoryStream(); + _binaryWriter = new BinaryWriter(_packetStream); + _parentPacketVersion = parentPacketVersion; + + string pipeName = $"MSBuild{EnvironmentUtilities.CurrentProcessId}"; + + SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; + var security = new PipeSecurity(); + + // Restrict access to just this account. We set the owner specifically here, and on the + // pipe client side they will check the owner against this one - they must have identical + // SIDs or the client will reject this server. This is used to avoid attacks where a + // hacked server creates a less restricted pipe in an attempt to lure us into using it and + // then sending build requests to the real pipe client (which is the MSBuild Build Manager.) + var rule = new PipeAccessRule(identifier, PipeAccessRights.ReadWrite, AccessControlType.Allow); + security.AddAccessRule(rule); + security.SetOwner(identifier); + + _pipeServer = new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.WriteThrough, + inBufferSize: PipeBufferSize, + outBufferSize: PipeBufferSize, + security, + HandleInheritability.None); + } + + /// + /// Raised when the link status has changed. + /// + public event LinkStatusChangedDelegate? OnLinkStatusChanged; + + /// + /// Returns the link status of this node. + /// + public LinkStatus LinkStatus => _status; + + /// + /// Causes this endpoint to wait for the remote endpoint to connect. + /// + /// The factory used to create packets. + public void Listen(INodePacketFactory factory) + { + ErrorUtilities.VerifyThrow(_status == LinkStatus.Inactive, "Link not inactive. Status is {0}", _status); + ErrorUtilities.VerifyThrowArgumentNull(factory); + + _packetFactory = factory; + + _isClientDisconnecting = false; + _packetPump = new Thread(PacketPumpProc) + { + IsBackground = true, + Name = "OutOfProc Endpoint Packet Pump" + }; + + _packetAvailable = new AutoResetEvent(false); + _terminatePacketPump = new AutoResetEvent(false); + _packetQueue = new ConcurrentQueue(); + _packetPump.Start(); + } + + /// + /// Causes this node to connect to the matched endpoint. + /// + /// The factory used to create packets. + public void Connect(INodePacketFactory factory) + => ErrorUtilities.ThrowInternalError("Connect() not valid on the out of proc endpoint."); + + /// + /// Shuts down the link. + /// + public void Disconnect() + { + ErrorUtilities.VerifyThrow(_packetPump != null, $"{nameof(_packetPump)} is null."); + ErrorUtilities.VerifyThrow(_packetPump.ManagedThreadId != Thread.CurrentThread.ManagedThreadId, "Can't join on the same thread."); + ErrorUtilities.VerifyThrow(_terminatePacketPump != null, $"{nameof(_terminatePacketPump)} is null."); + + _terminatePacketPump.Set(); + _packetPump.Join(); + _terminatePacketPump.Close(); + _pipeServer.Dispose(); + _packetPump = null; + ChangeLinkStatus(LinkStatus.Inactive); + } + + /// + /// Sends data to the peer endpoint. + /// + /// The packet to send. + public void SendData(INodePacket packet) + { + ErrorUtilities.VerifyThrowArgumentNull(packet); + + // PERF: Set up a priority system so logging packets are sent only when all other packet types have been sent. + if (_status == LinkStatus.Active) + { + ErrorUtilities.VerifyThrow(_packetQueue != null, $"{nameof(_packetQueue)} is null"); + ErrorUtilities.VerifyThrow(_packetAvailable != null, $"{nameof(_packetAvailable)} is null"); + + _packetQueue.Enqueue(packet); + _packetAvailable.Set(); + } + } + + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// + public void ClientWillDisconnect() + { + _isClientDisconnecting = true; + } + + /// + /// Updates the current link status if it has changed and notifies any registered delegates. + /// + /// The status the node should now be in. + private void ChangeLinkStatus(LinkStatus newStatus) + { + ErrorUtilities.VerifyThrow(_status != newStatus, "Attempting to change status to existing status {0}.", _status); + CommunicationsUtilities.Trace($"Changing link status from {_status} to {newStatus}"); + _status = newStatus; + OnLinkStatusChanged?.Invoke(this, newStatus); + } + + /// + /// This method handles the asynchronous message pump. It waits for messages to show up on the queue + /// and calls FireDataAvailable for each such packet. It will terminate when the terminate event is + /// set. + /// + private void PacketPumpProc() + { + ErrorUtilities.VerifyThrow(_packetQueue != null, $"{nameof(_packetQueue)} is null"); + ErrorUtilities.VerifyThrow(_terminatePacketPump != null, $"{nameof(_terminatePacketPump)} is null"); + ErrorUtilities.VerifyThrow(_packetAvailable != null, $"{nameof(_packetAvailable)} is null"); + + NamedPipeServerStream localPipeServer = _pipeServer; + + AutoResetEvent localPacketAvailable = _packetAvailable; + AutoResetEvent localTerminatePacketPump = _terminatePacketPump; + ConcurrentQueue localPacketQueue = _packetQueue; + + DateTime originalWaitStartTime = DateTime.UtcNow; + bool gotValidConnection = false; + while (!gotValidConnection) + { + gotValidConnection = true; + DateTime restartWaitTime = DateTime.UtcNow; + + // We only wait to wait the difference between now and the last original start time, in case we have multiple hosts attempting + // to attach. This prevents each attempt from resetting the timer. + TimeSpan usedWaitTime = restartWaitTime - originalWaitStartTime; + int waitTimeRemaining = Math.Max(0, CommunicationsUtilities.NodeConnectionTimeout - (int)usedWaitTime.TotalMilliseconds); + + try + { + // Wait for a connection + IAsyncResult resultForConnection = localPipeServer.BeginWaitForConnection(null, null); + CommunicationsUtilities.Trace($"Waiting for connection {waitTimeRemaining} ms..."); + bool connected = resultForConnection.AsyncWaitHandle.WaitOne(waitTimeRemaining, false); + if (!connected) + { + CommunicationsUtilities.Trace("Connection timed out waiting a host to contact us. Exiting comm thread."); + ChangeLinkStatus(LinkStatus.ConnectionFailed); + return; + } + + CommunicationsUtilities.Trace("Parent started connecting. Reading handshake from parent"); + localPipeServer.EndWaitForConnection(resultForConnection); + + // The handshake protocol is a series of int exchanges. The host sends us a each component, and we + // verify it. Afterwards, the host sends an "End of Handshake" signal, to which we respond in kind. + // Once the handshake is complete, both sides can be assured the other is ready to accept data. + Handshake handshake = new(CommunicationsUtilities.GetHandshakeOptions()); + try + { + HandshakeComponents handshakeComponents = handshake.RetrieveHandshakeComponents(); + + int index = 0; + foreach (var component in handshakeComponents.EnumerateComponents()) + { + byte? byteToAccept = index == 0 ? CommunicationsUtilities.HandshakeVersion : null; + + if (!_pipeServer.TryReadIntForHandshake( + byteToAccept, /* this will disconnect a < 16.8 host; it expects leading 00 or F5 or 06. 0x00 is a wildcard */ + out HandshakeResult result)) + { + CommunicationsUtilities.Trace($"Handshake failed with error: {result.ErrorMessage}"); + } + + if (!IsHandshakePartValid(component, result.Value)) + { + CommunicationsUtilities.Trace($"Handshake failed. Received {result.Value} from host for {component.Key} but expected {component.Value}. Probably the host is a different MSBuild build."); + _pipeServer.WriteIntForHandshake(index + 1); + gotValidConnection = false; + break; + } + + index++; + } + + if (gotValidConnection) + { + // To ensure that our handshake and theirs have the same number of bytes, receive and send a magic number indicating EOS. + if (_pipeServer.TryReadEndOfHandshakeSignal(false, out _)) + { + // Send supported PacketVersion after EndOfHandshakeSignal + // Based on this parent node decides how to communicate with the child. + if (_parentPacketVersion >= 2) + { + _pipeServer.WriteIntForHandshake(Handshake.PacketVersionFromChildMarker); // Marker: PacketVersion follows + _pipeServer.WriteIntForHandshake(NodePacketTypeExtensions.PacketVersion); + CommunicationsUtilities.Trace($"Sent PacketVersion: {NodePacketTypeExtensions.PacketVersion}"); + } + + CommunicationsUtilities.Trace("Successfully connected to parent."); + _pipeServer.WriteEndOfHandshakeSignal(); + + // We will only talk to a host that was started by the same user as us. Even though the pipe access is set to only allow this user, we want to ensure they + // haven't attempted to change those permissions out from under us. This ensures that the only way they can truly gain access is to be impersonating the + // user we were started by. + WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent(); + WindowsIdentity? clientIdentity = null; + localPipeServer.RunAsClient(() => { clientIdentity = WindowsIdentity.GetCurrent(true); }); + + if (clientIdentity == null || !string.Equals(clientIdentity.Name, currentIdentity.Name, StringComparison.OrdinalIgnoreCase)) + { + string clientIdentityName = clientIdentity != null ? clientIdentity.Name : ""; + CommunicationsUtilities.Trace($"Handshake failed. Host user is {clientIdentityName} but we were created by {currentIdentity.Name}."); + gotValidConnection = false; + continue; + } + } + } + } + catch (IOException e) + { + // We will get here when: + // 1. The host (OOP main node) connects to us, it immediately checks for user privileges + // and if they don't match it disconnects immediately leaving us still trying to read the blank handshake + // 2. The host is too old sending us bits we automatically reject in the handshake + // 3. We expected to read the EndOfHandshake signal, but we received something else + CommunicationsUtilities.Trace($"Client connection failed but we will wait for another connection. Exception: {e.Message}"); + + gotValidConnection = false; + } + catch (InvalidOperationException) + { + gotValidConnection = false; + } + + if (!gotValidConnection) + { + if (localPipeServer.IsConnected) + { + localPipeServer.Disconnect(); + } + + continue; + } + + ChangeLinkStatus(LinkStatus.Active); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + CommunicationsUtilities.Trace($"Client connection failed. Exiting comm thread. {e}"); + if (localPipeServer.IsConnected) + { + localPipeServer.Disconnect(); + } + + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + return; + } + } + + RunReadLoop( + new BufferedReadStream(_pipeServer), + _pipeServer, + localPacketQueue, + localPacketAvailable, + localTerminatePacketPump); + + CommunicationsUtilities.Trace("Ending read loop"); + + try + { + if (localPipeServer.IsConnected) + { + localPipeServer.WaitForPipeDrain(); + localPipeServer.Disconnect(); + } + } + catch (Exception) + { + // We don't really care if Disconnect somehow fails, but it gives us a chance to do the right thing. + } + } + + /// + /// Method to verify that the handshake part received from the host matches the expected values. + /// + private static bool IsHandshakePartValid(KeyValuePair component, int handshakePart) + { + if (handshakePart == component.Value) + { + return true; + } + + CommunicationsUtilities.Trace($"Handshake failed. Received {handshakePart} from host for {component.Key} but expected {component.Value}. Probably the host is a different MSBuild build."); + + return false; + } + + private void RunReadLoop( + BufferedReadStream localReadPipe, + NamedPipeServerStream localWritePipe, + ConcurrentQueue localPacketQueue, + AutoResetEvent localPacketAvailable, + AutoResetEvent localTerminatePacketPump) + { + ErrorUtilities.VerifyThrow(_packetFactory != null, $"{nameof(_packetFactory)} is null"); + + INodePacketFactory packetFactory = _packetFactory; + + // Ordering of the wait handles is important. The first signaled wait handle in the array + // will be returned by WaitAny if multiple wait handles are signaled. We prefer to have the + // terminate event triggered so that we cannot get into a situation where packets are being + // spammed to the endpoint and it never gets an opportunity to shutdown. + CommunicationsUtilities.Trace("Entering read loop."); + byte[] headerByte = new byte[5]; + ITranslator? writeTranslator = null; + IAsyncResult result = localReadPipe.BeginRead(headerByte, offset: 0, headerByte.Length, callback: null, state: null); + + // Ordering is important. We want packetAvailable to supercede terminate otherwise we will not properly wait for all + // packets to be sent by other threads which are shutting down, such as the logging thread. + WaitHandle[] handles = + [ + result.AsyncWaitHandle, + localPacketAvailable, + localTerminatePacketPump, + ]; + + bool exitLoop = false; + do + { + int waitId = WaitHandle.WaitAny(handles); + switch (waitId) + { + case 0: + { + int bytesRead; + try + { + bytesRead = localReadPipe.EndRead(result); + } + catch (Exception e) + { + // Lost communications. Abort (but allow node reuse) + CommunicationsUtilities.Trace($"Exception reading from server. {e}"); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Inactive); + exitLoop = true; + break; + } + + if (bytesRead != headerByte.Length) + { + // Incomplete read. Abort. + if (bytesRead == 0) + { + if (_isClientDisconnecting) + { + CommunicationsUtilities.Trace("Parent disconnected gracefully."); + + // Do not change link status to failed as this could make node think connection has failed + // and recycle node, while this is perfectly expected and handled race condition + // (both client and node is about to close pipe and client can be faster). + } + else + { + CommunicationsUtilities.Trace("Parent disconnected abruptly."); + ChangeLinkStatus(LinkStatus.Failed); + } + } + else + { + CommunicationsUtilities.Trace($"Incomplete header read from server. {bytesRead} of {headerByte.Length} bytes read"); + ChangeLinkStatus(LinkStatus.Failed); + } + + exitLoop = true; + break; + } + + // Check if this packet has an extended header that includes a version part. + byte rawType = headerByte[0]; + bool hasExtendedHeader = NodePacketTypeExtensions.HasExtendedHeader(rawType); + NodePacketType packetType = hasExtendedHeader + ? NodePacketTypeExtensions.GetNodePacketType(rawType) + : (NodePacketType)rawType; + + byte parentVersion = 0; + if (hasExtendedHeader) + { + parentVersion = NodePacketTypeExtensions.ReadVersion(localReadPipe); + } + + try + { + ITranslator readTranslator = BinaryTranslator.GetReadTranslator(localReadPipe, _sharedReadBuffer); + + // parent sends a packet version that is already negotiated during handshake. + readTranslator.NegotiatedPacketVersion = parentVersion; + packetFactory.DeserializeAndRoutePacket(0, packetType, readTranslator); + } + catch (Exception e) + { + // Error while deserializing or handling packet. Abort. + CommunicationsUtilities.Trace($"Exception while deserializing packet {packetType}: {e}"); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + break; + } + + result = localReadPipe.BeginRead(headerByte, 0, headerByte.Length, null, null); + + handles[0] = result.AsyncWaitHandle; + } + + break; + + case 1: + case 2: + try + { + // Write out all the queued packets. + while (localPacketQueue.TryDequeue(out INodePacket? packet)) + { + var packetStream = _packetStream; + packetStream.SetLength(0); + + // Re-use writeTranslator; we clear _packetStream but never replace it. + // If _packetStream is ever reassigned, set writeTranslator = null first. + writeTranslator ??= BinaryTranslator.GetWriteTranslator(packetStream); + + packetStream.WriteByte((byte)packet.Type); + + // Pad for packet length + _binaryWriter.Write(0); + + // Reset the position in the write buffer. + packet.Translate(writeTranslator); + + int packetStreamLength = (int)packetStream.Position; + + // Now write in the actual packet length + packetStream.Position = 1; + _binaryWriter.Write(packetStreamLength - 5); + + localWritePipe.Write(packetStream.GetBuffer(), 0, packetStreamLength); + } + } + catch (Exception e) + { + // Error while deserializing or handling packet. Abort. + CommunicationsUtilities.Trace($"Exception while serializing packets: {e}"); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + break; + } + + if (waitId == 2) + { + CommunicationsUtilities.Trace("Disconnecting voluntarily"); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + } + + break; + + default: + ErrorUtilities.ThrowInternalError($"waitId {waitId} out of range."); + break; + } + } + while (!exitLoop); + } +} diff --git a/src/MSBuildTaskHost/BackEnd/NodeEngineShutdownReason.cs b/src/MSBuildTaskHost/BackEnd/NodeEngineShutdownReason.cs new file mode 100644 index 00000000000..52d6ee5b2ca --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/NodeEngineShutdownReason.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Reasons for a node to shutdown. +/// +internal enum NodeEngineShutdownReason +{ + /// + /// The BuildManager sent a command instructing the node to terminate. + /// + BuildComplete, + + /// + /// The BuildManager sent a command instructing the node to terminate, but to restart for reuse. + /// + BuildCompleteReuse, + + /// + /// The communication link failed. + /// + ConnectionFailed, + + /// + /// The NodeEngine caught an exception which requires the Node to shut down. + /// + Error, +} diff --git a/src/MSBuildTaskHost/BackEnd/NodePacketFactory.cs b/src/MSBuildTaskHost/BackEnd/NodePacketFactory.cs new file mode 100644 index 00000000000..e763efe3d85 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/NodePacketFactory.cs @@ -0,0 +1,95 @@ +// 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 Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Implementation of INodePacketFactory as a helper class for classes which expose this interface publicly. +/// +internal sealed class NodePacketFactory : INodePacketFactory +{ + /// + /// Mapping of packet types to factory information. + /// + private readonly Dictionary _packetFactories; + + public NodePacketFactory() + { + _packetFactories = new Dictionary(); + } + + /// + /// Registers a packet handler. + /// + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + => _packetFactories[packetType] = new PacketFactoryRecord(handler, factory); + + /// + /// Unregisters a packet handler. + /// + public void UnregisterPacketHandler(NodePacketType packetType) + => _packetFactories.Remove(packetType); + + /// + /// Creates and routes a packet with data from a binary stream. + /// + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) + { + if (!_packetFactories.TryGetValue(packetType, out PacketFactoryRecord? record)) + { + ErrorUtilities.ThrowInternalError($"No packet handler for type {packetType}"); + } + + INodePacket packet = record.DeserializePacket(translator); + record.RoutePacket(nodeId, packet); + } + + /// + /// Creates a packet with data from a binary stream. + /// + public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) + { + if (!_packetFactories.TryGetValue(packetType, out PacketFactoryRecord? record)) + { + ErrorUtilities.ThrowInternalError($"No packet handler for type {packetType}"); + } + + return record.DeserializePacket(translator); + } + + /// + /// Routes the specified packet. + /// + public void RoutePacket(int nodeId, INodePacket packet) + { + if (!_packetFactories.TryGetValue(packet.Type, out PacketFactoryRecord record)) + { + ErrorUtilities.ThrowInternalError($"No packet handler for type {packet.Type}"); + } + + record.RoutePacket(nodeId, packet); + } + + /// + /// A record for a packet factory. + /// + /// The handler to invoke when the packet is deserialized. + /// The method used to construct a packet from a translator stream. + private sealed class PacketFactoryRecord(INodePacketHandler handler, NodePacketFactoryMethod factoryMethod) + { + /// + /// Creates a packet from a binary stream. + /// + public INodePacket DeserializePacket(ITranslator translator) + => factoryMethod(translator); + + /// + /// Routes the packet to the correct destination. + /// + public void RoutePacket(int nodeId, INodePacket packet) + => handler.PacketReceived(nodeId, packet); + } +} diff --git a/src/MSBuildTaskHost/BackEnd/NodeShutdown.cs b/src/MSBuildTaskHost/BackEnd/NodeShutdown.cs new file mode 100644 index 00000000000..27e27c9dd51 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/NodeShutdown.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Reasons why the node shut down. +/// +internal enum NodeShutdownReason +{ + /// + /// The node shut down because it was requested to shut down. + /// + Requested, + + /// + /// The node shut down because of an error. + /// + Error, + + /// + /// The node shut down because the connection failed. + /// + ConnectionFailed, +} + +/// +/// Implementation of INodePacket for the packet informing the build manager than a node has shut down. +/// This is the last packet the BuildManager will receive from a Node, and as such can be used to trigger +/// any appropriate cleanup behavior. +/// +internal sealed class NodeShutdown : INodePacket +{ + /// + /// The reason the node shut down. + /// + private NodeShutdownReason _reason; + + /// + /// The exception - if any. + /// + private Exception? _exception; + + /// + /// Constructor + /// + public NodeShutdown(NodeShutdownReason reason) + : this(reason, exception: null) + { + } + + public NodeShutdown(NodeShutdownReason reason, Exception? exception) + { + _reason = reason; + _exception = exception; + } + + private NodeShutdown() + { + } + + /// + /// Gets the packet type. + /// + public NodePacketType Type => NodePacketType.NodeShutdown; + + /// + /// Gets the reason for shutting down. + /// + public NodeShutdownReason Reason => _reason; + + /// + /// Gets the exception, if any. + /// + public Exception? Exception => _exception; + + /// + /// Serializes or deserializes a packet. + /// + public void Translate(ITranslator translator) + { + translator.TranslateEnum(ref _reason, (int)_reason); + translator.TranslateException(ref _exception); + } + + internal static NodeShutdown FactoryForDeserialization(ITranslator translator) + { + var shutdown = new NodeShutdown(); + shutdown.Translate(translator); + return shutdown; + } +} diff --git a/src/MSBuildTaskHost/BackEnd/TaskHostConfiguration.cs b/src/MSBuildTaskHost/BackEnd/TaskHostConfiguration.cs new file mode 100644 index 00000000000..d9c8172228d --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TaskHostConfiguration.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// TaskHostConfiguration contains information needed for the task host to +/// configure itself for to execute a particular task. +/// +internal sealed class TaskHostConfiguration : INodePacket +{ + /// + /// The node id (of the parent node, to make the logging work out). + /// + private int _nodeId; + + /// + /// The startup directory. + /// + private string? _startupDirectory; + + /// + /// The process environment. + /// + private Dictionary? _buildProcessEnvironment; + + /// + /// The culture. + /// + private CultureInfo? _culture = CultureInfo.CurrentCulture; + + /// + /// The UI culture. + /// + private CultureInfo? _uiCulture = CultureInfo.CurrentUICulture; + + /// + /// The AppDomainSetup that we may want to use on AppDomainIsolated tasks. + /// + private AppDomainSetup? _appDomainSetup; + + /// + /// Line number where the instance of this task is defined. + /// + private int _lineNumberOfTask; + + /// + /// Column number where the instance of this task is defined. + /// + private int _columnNumberOfTask; + + /// + /// Project file where the instance of this task is defined. + /// + private string? _projectFileOfTask; + + /// + /// ContinueOnError flag for this particular task. + /// + private bool _continueOnError; + + /// + /// Name of the task to be executed on the task host. + /// + private string? _taskName; + + /// + /// Location of the assembly containing the task to be executed. + /// + private string? _taskLocation; + + /// + /// Whether task inputs are logged. + /// + private bool _isTaskInputLoggingEnabled; + + /// + /// Target name that is requesting the task execution. + /// + private string? _targetName; + + /// + /// Project file path that is requesting the task execution. + /// + private string? _projectFile; + + /// + /// The set of parameters to apply to the task prior to execution. + /// + private Dictionary? _taskParameters; + + private Dictionary? _globalParameters; + + private ICollection? _warningsAsErrors; + private ICollection? _warningsNotAsErrors; + + private ICollection? _warningsAsMessages; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the node being configured. + /// The startup directory for the task being executed. + /// The set of environment variables to apply to the task execution process. + /// The culture of the thread that will execute the task. + /// The UI culture of the thread that will execute the task. + /// The host services to be used by the task host. + /// The AppDomainSetup that may be used to pass information to an AppDomainIsolated task. + /// The line number of the location from which this task was invoked. + /// The column number of the location from which this task was invoked. + /// The project file from which this task was invoked. + /// A flag to indicate whether to continue with the build after the task fails. + /// The name of the task. + /// The location of the assembly from which the task is to be loaded. + /// The name of the target that is requesting the task execution. + /// The project path that invokes the task. + /// A flag to indicate whether task inputs are logged. + /// The parameters to apply to the task. + /// The global properties for the current project. + /// A collection of warning codes to be treated as errors. + /// A collection of warning codes not to be treated as errors. + /// A collection of warning codes to be treated as messages. + public TaskHostConfiguration( + int nodeId, + string startupDirectory, + Dictionary buildProcessEnvironment, + CultureInfo culture, + CultureInfo uiCulture, + AppDomainSetup appDomainSetup, + int lineNumberOfTask, + int columnNumberOfTask, + string projectFileOfTask, + bool continueOnError, + string taskName, + string taskLocation, + string targetName, + string projectFile, + bool isTaskInputLoggingEnabled, + Dictionary taskParameters, + Dictionary globalParameters, + ICollection warningsAsErrors, + ICollection warningsNotAsErrors, + ICollection warningsAsMessages) + { + ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName)); + ErrorUtilities.VerifyThrowInternalLength(taskLocation, nameof(taskLocation)); + + _nodeId = nodeId; + _startupDirectory = startupDirectory; + + _buildProcessEnvironment = buildProcessEnvironment; + + _culture = culture; + _uiCulture = uiCulture; + _appDomainSetup = appDomainSetup; + _lineNumberOfTask = lineNumberOfTask; + _columnNumberOfTask = columnNumberOfTask; + _projectFileOfTask = projectFileOfTask; + _projectFile = projectFile; + _continueOnError = continueOnError; + _taskName = taskName; + _taskLocation = taskLocation; + _targetName = targetName; + _isTaskInputLoggingEnabled = isTaskInputLoggingEnabled; + _warningsAsErrors = warningsAsErrors; + _warningsNotAsErrors = warningsNotAsErrors; + _warningsAsMessages = warningsAsMessages; + + if (taskParameters != null) + { + _taskParameters = new(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair parameter in taskParameters) + { + _taskParameters[parameter.Key] = new TaskParameter(parameter.Value); + } + } + + _globalParameters = globalParameters ?? []; + } + + private TaskHostConfiguration() + { + } + + /// + /// Gets the node id. + /// + public int NodeId => _nodeId; + + /// + /// Gets the startup directory. + /// + public string StartupDirectory => _startupDirectory!; + + /// + /// Gets the process environment. + /// + public Dictionary BuildProcessEnvironment => _buildProcessEnvironment!; + + /// + /// Gets the culture. + /// + public CultureInfo? Culture => _culture; + + /// + /// Gets the UI culture. + /// + public CultureInfo? UICulture => _uiCulture; + + /// + /// Gets the AppDomain configuration bytes that we may want to use to initialize + /// AppDomainIsolated tasks. + /// + public AppDomainSetup AppDomainSetup => _appDomainSetup!; + + /// + /// Gets the line number where the instance of this task is defined. + /// + public int LineNumberOfTask => _lineNumberOfTask; + + /// + /// Gets the column number where the instance of this task is defined. + /// + public int ColumnNumberOfTask => _columnNumberOfTask; + + /// + /// Gets the project file path that is requesting the task execution. + /// + public string? ProjectFile => _projectFile; + + /// + /// Gets the target name that is requesting the task execution. + /// + public string? TargetName => _targetName; + + /// + /// Gets the ContinueOnError flag for this particular task. + /// + public bool ContinueOnError => _continueOnError; + + /// + /// Gets the project file where the instance of this task is defined. + /// + public string ProjectFileOfTask => _projectFileOfTask!; + + /// + /// Gets the name of the task to execute. + /// + public string TaskName => _taskName!; + + /// + /// Gets the path to the assembly to load the task from. + /// + public string TaskLocation => _taskLocation!; + + /// + /// Returns if the build is configured to log all task inputs. + /// + public bool IsTaskInputLoggingEnabled => _isTaskInputLoggingEnabled; + + /// + /// Gets the parameters to set on the instantiated task prior to execution. + /// + public Dictionary TaskParameters => _taskParameters!; + + /// + /// Gets the global properties for the current project. + /// + public Dictionary? GlobalProperties => _globalParameters; + + /// + /// Gets the NodePacketType of this NodePacket. + /// + public NodePacketType Type => NodePacketType.TaskHostConfiguration; + + public ICollection? WarningsAsErrors => _warningsAsErrors; + + public ICollection? WarningsNotAsErrors => _warningsNotAsErrors; + + public ICollection? WarningsAsMessages => _warningsAsMessages; + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + translator.Translate(ref _nodeId); + translator.Translate(ref _startupDirectory); + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + translator.TranslateCulture(ref _culture); + translator.TranslateCulture(ref _uiCulture); + + // The packet version is used to determine if the AppDomain configuration should be serialized. + // If the packet version is bigger then 0, it means the task host will running under .NET. + // Although MSBuild.exe runs under .NET Framework and has AppDomain support, + // we don't transmit AppDomain config when communicating with dotnet.exe (it is not supported in .NET 5+). + if (translator.NegotiatedPacketVersion == 0) + { + byte[]? appDomainConfigBytes = null; + + // Set the configuration bytes just before serialization in case the SetConfigurationBytes was invoked during lifetime of this instance. + if (translator.Mode == TranslationDirection.WriteToStream) + { + appDomainConfigBytes = _appDomainSetup!.GetConfigurationBytes(); + } + + translator.Translate(ref appDomainConfigBytes); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + _appDomainSetup = new AppDomainSetup(); + _appDomainSetup.SetConfigurationBytes(appDomainConfigBytes); + } + } + + translator.Translate(ref _lineNumberOfTask); + translator.Translate(ref _columnNumberOfTask); + translator.Translate(ref _projectFileOfTask); + translator.Translate(ref _taskName); + translator.Translate(ref _taskLocation); + if (translator.NegotiatedPacketVersion >= 2) + { + translator.Translate(ref _targetName); + translator.Translate(ref _projectFile); + } + + translator.Translate(ref _isTaskInputLoggingEnabled); + translator.TranslateDictionary(ref _taskParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); + translator.Translate(ref _continueOnError); + translator.TranslateDictionary(ref _globalParameters, StringComparer.OrdinalIgnoreCase); + translator.Translate(collection: ref _warningsAsErrors, + objectTranslator: (t, ref s) => t.Translate(ref s!), + collectionFactory: count => new HashSet(StringComparer.OrdinalIgnoreCase)); + translator.Translate(collection: ref _warningsNotAsErrors, + objectTranslator: (t, ref s) => t.Translate(ref s!), + collectionFactory: count => new HashSet(StringComparer.OrdinalIgnoreCase)); + translator.Translate(collection: ref _warningsAsMessages, + objectTranslator: (t, ref s) => t.Translate(ref s!), + collectionFactory: count => new HashSet(StringComparer.OrdinalIgnoreCase)); + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + var configuration = new TaskHostConfiguration(); + configuration.Translate(translator); + + return configuration; + } +} diff --git a/src/MSBuildTaskHost/BackEnd/TaskHostTaskCancelled.cs b/src/MSBuildTaskHost/BackEnd/TaskHostTaskCancelled.cs new file mode 100644 index 00000000000..6f38556f6f3 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TaskHostTaskCancelled.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// TaskHostTaskCancelled informs the task host that the task it is +/// currently executing has been canceled. +/// +internal sealed class TaskHostTaskCancelled : INodePacket +{ + public TaskHostTaskCancelled() + { + } + + /// + /// Gets the type of this NodePacket. + /// + public NodePacketType Type => NodePacketType.TaskHostTaskCancelled; + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + // Do nothing -- this packet doesn't contain any parameters. + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + var taskCancelled = new TaskHostTaskCancelled(); + taskCancelled.Translate(translator); + return taskCancelled; + } +} diff --git a/src/MSBuildTaskHost/BackEnd/TaskHostTaskComplete.cs b/src/MSBuildTaskHost/BackEnd/TaskHostTaskComplete.cs new file mode 100644 index 00000000000..1dd88584e3c --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TaskHostTaskComplete.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// How the task completed -- successful, failed, or crashed. +/// +internal enum TaskCompleteType +{ + /// + /// Task execution succeeded + /// + Success, + + /// + /// Task execution failed + /// + Failure, + + /// + /// Task crashed during initialization steps -- loading the task, + /// validating or setting the parameters, etc. + /// + CrashedDuringInitialization, + + /// + /// Task crashed while being executed + /// + CrashedDuringExecution, + + /// + /// Task crashed after being executed + /// -- Getting outputs, etc + /// + CrashedAfterExecution +} + +/// +/// TaskHostTaskComplete contains all the information the parent node +/// needs from the task host on completion of task execution. +/// +internal sealed class TaskHostTaskComplete : INodePacket +{ + /// + /// Result of the task's execution. + /// + private TaskCompleteType _taskResult; + + /// + /// If the task threw an exception during its initialization or execution, + /// save it here. + /// + private Exception? _taskException; + + /// + /// If there's an additional message that should be attached to the error + /// logged beyond "task X failed unexpectedly", save it here. May be null. + /// + private string? _taskExceptionMessage; + + /// + /// If the message saved in taskExceptionMessage requires arguments, save + /// them here. May be null. + /// + private string[]? _taskExceptionMessageArgs; + + /// + /// The set of parameters / values from the task after it finishes execution. + /// + private Dictionary? _taskOutputParameters; + + /// + /// The process environment at the end of task execution. + /// + private Dictionary? _buildProcessEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// The result of the task's execution. + /// The build process environment as it was at the end of the task's execution. + public TaskHostTaskComplete( + OutOfProcTaskHostTaskResult result, + Dictionary? buildProcessEnvironment) + { + ErrorUtilities.VerifyThrowInternalNull(result); + + _taskResult = result.Result; + _taskException = result.TaskException; + _taskExceptionMessage = result.ExceptionMessage; + _taskExceptionMessageArgs = result.ExceptionMessageArgs; + + if (result.FinalParameterValues != null) + { + _taskOutputParameters = new(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair parameter in result.FinalParameterValues) + { + _taskOutputParameters[parameter.Key] = new TaskParameter(parameter.Value); + } + } + + _buildProcessEnvironment = buildProcessEnvironment; + } + + private TaskHostTaskComplete() + { + } + + /// + /// Gets the result of the task's execution. + /// + public TaskCompleteType TaskResult => _taskResult; + + /// + /// Gets the exception thrown be the task during initialization or execution, if any. + /// save it here. + /// + public Exception? TaskException => _taskException; + + /// + /// If there's an additional message that should be attached to the error + /// logged beyond "task X failed unexpectedly", put it here. May be null. + /// + public string? TaskExceptionMessage => _taskExceptionMessage; + + /// + /// If there are arguments that need to be formatted into the message being + /// sent, set them here. May be null. + /// + public string[]? TaskExceptionMessageArgs => _taskExceptionMessageArgs; + + /// + /// Task parameters and their values after the task has finished. + /// + public Dictionary? TaskOutputParameters => _taskOutputParameters ??= new(StringComparer.OrdinalIgnoreCase); + + /// + /// The process environment. + /// + public Dictionary? BuildProcessEnvironment => _buildProcessEnvironment; + + /// + /// Gets the type of this packet. + /// + public NodePacketType Type => NodePacketType.TaskHostTaskComplete; + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + translator.TranslateEnum(ref _taskResult, (int)_taskResult); + translator.TranslateException(ref _taskException); + translator.Translate(ref _taskExceptionMessage); + translator.Translate(ref _taskExceptionMessageArgs); + translator.TranslateDictionary(ref _taskOutputParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + bool hasFileAccessData = false; + translator.Translate(ref hasFileAccessData); + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + var taskComplete = new TaskHostTaskComplete(); + taskComplete.Translate(translator); + return taskComplete; + } +} diff --git a/src/MSBuildTaskHost/BackEnd/TaskParameter.cs b/src/MSBuildTaskHost/BackEnd/TaskParameter.cs new file mode 100644 index 00000000000..cae9c289530 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TaskParameter.cs @@ -0,0 +1,766 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Security; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.Resources; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Type of parameter, used to figure out how to serialize it. +/// +internal enum TaskParameterType +{ + /// + /// Parameter is null. + /// + Null, + + /// + /// Parameter is of a type described by a . + /// + PrimitiveType, + + /// + /// Parameter is an array of a type described by a . + /// + PrimitiveTypeArray, + + /// + /// Parameter is a value type. Note: Must be . + /// + ValueType, + + /// + /// Parameter is an array of value types. Note: Must be . + /// + ValueTypeArray, + + /// + /// Parameter is an ITaskItem. + /// + ITaskItem, + + /// + /// Parameter is an array of ITaskItems. + /// + ITaskItemArray, + + /// + /// An invalid parameter -- the value of this parameter contains the exception + /// that is thrown when trying to access it. + /// + Invalid, +} + +/// +/// Wrapper for task parameters, to allow proper serialization even +/// in cases where the parameter is not .NET serializable. +/// +internal class TaskParameter : MarshalByRefObject, ITranslatable +{ + /// + /// The TaskParameterType of the wrapped parameter. + /// + private TaskParameterType _parameterType; + + /// + /// The of the wrapped parameter if it's a primitive type. + /// + private TypeCode _parameterTypeCode; + + /// + /// The actual task parameter that we're wrapping + /// + private object? _wrappedParameter; + + public TaskParameter(object? wrappedParameter) + { + if (wrappedParameter == null) + { + _parameterType = TaskParameterType.Null; + _wrappedParameter = null; + return; + } + + Type wrappedParameterType = wrappedParameter.GetType(); + + if (wrappedParameter is Exception) + { + _parameterType = TaskParameterType.Invalid; + _wrappedParameter = wrappedParameter; + return; + } + + // It's not null or invalid, so it should be a valid parameter type. + ErrorUtilities.VerifyThrow( + TaskParameterTypeVerifier.IsValidInputParameter(wrappedParameterType) || TaskParameterTypeVerifier.IsValidOutputParameter(wrappedParameterType), + "How did we manage to get a task parameter of type {0} that isn't a valid parameter type?", + wrappedParameterType); + + if (wrappedParameterType.IsArray) + { + TypeCode typeCode = Type.GetTypeCode(wrappedParameterType.GetElementType()); + if (typeCode != TypeCode.Object && typeCode != TypeCode.DBNull) + { + _parameterType = TaskParameterType.PrimitiveTypeArray; + _parameterTypeCode = typeCode; + _wrappedParameter = wrappedParameter; + } + else if (typeof(ITaskItem[]).IsAssignableFrom(wrappedParameterType)) + { + _parameterType = TaskParameterType.ITaskItemArray; + ITaskItem[] inputAsITaskItemArray = (ITaskItem[])wrappedParameter; + ITaskItem[] taskItemArrayParameter = new ITaskItem[inputAsITaskItemArray.Length]; + + for (int i = 0; i < inputAsITaskItemArray.Length; i++) + { + if (inputAsITaskItemArray[i] != null) + { + taskItemArrayParameter[i] = new TaskParameterTaskItem(inputAsITaskItemArray[i]); + } + } + + _wrappedParameter = taskItemArrayParameter; + } + else if (wrappedParameterType.GetElementType().IsValueType) + { + _parameterType = TaskParameterType.ValueTypeArray; + _wrappedParameter = wrappedParameter; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + else + { + // scalar parameter + // Preserve enums as strings: the enum type itself may not + // be loaded on the other side of the serialization, but + // we would convert to string anyway after pulling the + // task output into a property or item. + if (wrappedParameterType.IsEnum) + { + wrappedParameter = (string)Convert.ChangeType(wrappedParameter, typeof(string), CultureInfo.InvariantCulture); + wrappedParameterType = typeof(string); + } + + TypeCode typeCode = Type.GetTypeCode(wrappedParameterType); + if (typeCode != TypeCode.Object && typeCode != TypeCode.DBNull) + { + _parameterType = TaskParameterType.PrimitiveType; + _parameterTypeCode = typeCode; + _wrappedParameter = wrappedParameter; + } + else if (typeof(ITaskItem).IsAssignableFrom(wrappedParameterType)) + { + _parameterType = TaskParameterType.ITaskItem; + _wrappedParameter = new TaskParameterTaskItem((ITaskItem)wrappedParameter); + } + else if (wrappedParameterType.IsValueType) + { + _parameterType = TaskParameterType.ValueType; + _wrappedParameter = wrappedParameter; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + } + + /// + /// Constructor for deserialization. + /// + private TaskParameter() + { + } + + /// + /// Gets the of the wrapped parameter. + /// + public TaskParameterType ParameterType => _parameterType; + + /// + /// Gets the of the wrapper parameter if it's a primitive or array of primitives. + /// + public TypeCode ParameterTypeCode => _parameterTypeCode; + + /// + /// Gets the actual task parameter that we're wrapping. + /// + public object? WrappedParameter => _wrappedParameter; + + /// + /// TaskParameter's ToString should just pass through to whatever it's wrapping. + /// + public override string ToString() + => _wrappedParameter is null ? string.Empty : _wrappedParameter.ToString(); + + /// + /// Serialize / deserialize this item. + /// + public void Translate(ITranslator translator) + { + translator.TranslateEnum(ref _parameterType, (int)_parameterType); + + switch (_parameterType) + { + case TaskParameterType.Null: + _wrappedParameter = null; + break; + case TaskParameterType.PrimitiveType: + TranslatePrimitiveType(translator); + break; + case TaskParameterType.PrimitiveTypeArray: + TranslatePrimitiveTypeArray(translator); + break; + case TaskParameterType.ValueType: + TranslateValueType(translator); + break; + case TaskParameterType.ValueTypeArray: + TranslateValueTypeArray(translator); + break; + case TaskParameterType.ITaskItem: + TranslateITaskItem(translator); + break; + case TaskParameterType.ITaskItemArray: + TranslateITaskItemArray(translator); + break; + case TaskParameterType.Invalid: + var exceptionParam = (Exception?)_wrappedParameter; + translator.TranslateException(ref exceptionParam); + _wrappedParameter = exceptionParam; + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object? InitializeLifetimeService() => null; + + /// + /// Factory for deserialization. + /// + internal static TaskParameter FactoryForDeserialization(ITranslator translator) + { + TaskParameter taskParameter = new(); + taskParameter.Translate(translator); + return taskParameter; + } + + /// + /// Serialize / deserialize this item. + /// + private void TranslateITaskItemArray(ITranslator translator) + { + var wrappedItems = (ITaskItem[]?)_wrappedParameter; + int length = wrappedItems?.Length ?? 0; + translator.Translate(ref length); + wrappedItems ??= new ITaskItem[length]; + + for (int i = 0; i < wrappedItems.Length; i++) + { + TaskParameterTaskItem taskItem = (TaskParameterTaskItem)wrappedItems[i]; + translator.Translate(ref taskItem, TaskParameterTaskItem.FactoryForDeserialization); + wrappedItems[i] = taskItem; + } + + _wrappedParameter = wrappedItems; + } + + /// + /// Serialize / deserialize this item. + /// + private void TranslateITaskItem(ITranslator translator) + { + TaskParameterTaskItem taskItem = (TaskParameterTaskItem)_wrappedParameter!; + translator.Translate(ref taskItem, TaskParameterTaskItem.FactoryForDeserialization); + _wrappedParameter = taskItem; + } + + /// + /// Serializes or deserializes a primitive type value wrapped by this . + /// + private void TranslatePrimitiveType(ITranslator translator) + { + translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode); + + switch (_parameterTypeCode) + { + case TypeCode.Boolean: + bool boolParam = _wrappedParameter is bool wrappedBool ? wrappedBool : default; + translator.Translate(ref boolParam); + _wrappedParameter = boolParam; + break; + + case TypeCode.Byte: + byte byteParam = _wrappedParameter is byte wrappedByte ? wrappedByte : default; + translator.Translate(ref byteParam); + _wrappedParameter = byteParam; + break; + + case TypeCode.Int16: + short shortParam = _wrappedParameter is short wrappedShort ? wrappedShort : default; + translator.Translate(ref shortParam); + _wrappedParameter = shortParam; + break; + + case TypeCode.UInt16: + ushort ushortParam = _wrappedParameter is ushort wrappedUShort ? wrappedUShort : default; + translator.Translate(ref ushortParam); + _wrappedParameter = ushortParam; + break; + + case TypeCode.Int64: + long longParam = _wrappedParameter is long wrappedLong ? wrappedLong : default; + translator.Translate(ref longParam); + _wrappedParameter = longParam; + break; + + case TypeCode.Double: + double doubleParam = _wrappedParameter is double wrappedDouble ? wrappedDouble : default; + translator.Translate(ref doubleParam); + _wrappedParameter = doubleParam; + break; + + case TypeCode.String: + string? stringParam = (string?)_wrappedParameter; + translator.Translate(ref stringParam); + _wrappedParameter = stringParam; + break; + + case TypeCode.DateTime: + DateTime dateTimeParam = _wrappedParameter is DateTime wrappedDateTime ? wrappedDateTime : default; + translator.Translate(ref dateTimeParam); + _wrappedParameter = dateTimeParam; + break; + + default: + // Fall back to converting to/from string for types that don't have ITranslator support. + string? stringValue = null; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + stringValue = (string)Convert.ChangeType(_wrappedParameter, typeof(string), CultureInfo.InvariantCulture); + } + + translator.Translate(ref stringValue); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + _wrappedParameter = Convert.ChangeType(stringValue, _parameterTypeCode, CultureInfo.InvariantCulture); + } + + break; + } + } + + /// + /// Serializes or deserializes an array of primitive type values wrapped by this . + /// + private void TranslatePrimitiveTypeArray(ITranslator translator) + { + translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode); + + switch (_parameterTypeCode) + { + case TypeCode.Boolean: + bool[]? boolArrayParam = (bool[]?)_wrappedParameter; + translator.Translate(ref boolArrayParam); + _wrappedParameter = boolArrayParam; + break; + + case TypeCode.Int32: + int[]? intArrayParam = (int[]?)_wrappedParameter; + translator.Translate(ref intArrayParam); + _wrappedParameter = intArrayParam; + break; + + case TypeCode.String: + string[]? stringArrayParam = (string[]?)_wrappedParameter; + translator.Translate(ref stringArrayParam); + _wrappedParameter = stringArrayParam; + break; + + default: + // Fall back to converting to/from string for types that don't have ITranslator support. + if (translator.Mode == TranslationDirection.WriteToStream) + { + Array array = (Array)_wrappedParameter!; + int length = array.Length; + + translator.Translate(ref length); + + for (int i = 0; i < length; i++) + { + string? valueString = Convert.ToString(array.GetValue(i), CultureInfo.InvariantCulture); + translator.Translate(ref valueString); + } + } + else + { + Type elementType = _parameterTypeCode switch + { + TypeCode.Char => typeof(char), + TypeCode.SByte => typeof(sbyte), + TypeCode.Byte => typeof(byte), + TypeCode.Int16 => typeof(short), + TypeCode.UInt16 => typeof(ushort), + TypeCode.UInt32 => typeof(uint), + TypeCode.Int64 => typeof(long), + TypeCode.UInt64 => typeof(ulong), + TypeCode.Single => typeof(float), + TypeCode.Double => typeof(double), + TypeCode.Decimal => typeof(decimal), + TypeCode.DateTime => typeof(DateTime), + _ => throw new NotImplementedException(), + }; + + int length = 0; + translator.Translate(ref length); + + Array array = Array.CreateInstance(elementType, length); + for (int i = 0; i < length; i++) + { + string? valueString = null; + translator.Translate(ref valueString); + array.SetValue(Convert.ChangeType(valueString, _parameterTypeCode, CultureInfo.InvariantCulture), i); + } + + _wrappedParameter = array; + } + + break; + } + } + + /// + /// Serializes or deserializes the value type instance wrapped by this . + /// + /// + /// The value type is converted to/from string using the class. Note that we require + /// task parameter types to be so this conversion is guaranteed to work for parameters + /// that have made it this far. + /// + private void TranslateValueType(ITranslator translator) + { + string? valueString = null; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + valueString = (string)Convert.ChangeType(_wrappedParameter, typeof(string), CultureInfo.InvariantCulture); + } + + translator.Translate(ref valueString); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + // We don't know how to convert the string back to the original value type. This is fine because output + // task parameters are anyway converted to strings by the engine (see TaskExecutionHost.GetValueOutputs) + // and input task parameters of custom value types are not supported. + _wrappedParameter = valueString; + } + } + + /// + /// Serializes or deserializes the value type array instance wrapped by this . + /// + /// + /// The array is assumed to be non-null. + /// + private void TranslateValueTypeArray(ITranslator translator) + { + if (translator.Mode == TranslationDirection.WriteToStream) + { + var array = (Array)_wrappedParameter!; + int length = array.Length; + + translator.Translate(ref length); + + for (int i = 0; i < length; i++) + { + string? valueString = Convert.ToString(array.GetValue(i), CultureInfo.InvariantCulture); + translator.Translate(ref valueString); + } + } + else + { + int length = 0; + translator.Translate(ref length); + + string?[] stringArray = new string[length]; + for (int i = 0; i < length; i++) + { + translator.Translate(ref stringArray[i]); + } + + // We don't know how to convert the string array back to the original value type array. + // This is fine because the engine would eventually convert it to strings anyway. + _wrappedParameter = stringArray; + } + } + + /// + /// Super simple ITaskItem derivative that we can use as a container for read items. + /// + private class TaskParameterTaskItem : MarshalByRefObject, ITaskItem, ITranslatable + { + private string? _escapedItemSpec; + private string? _escapedDefiningProject; + private Dictionary? _customEscapedMetadata; + private string? _fullPath; + + internal TaskParameterTaskItem(ITaskItem copyFrom) + { + if (copyFrom is TaskParameterTaskItem taskParameterTaskItem) + { + _escapedItemSpec = taskParameterTaskItem._escapedItemSpec; + _escapedDefiningProject = taskParameterTaskItem.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + _customEscapedMetadata = new(taskParameterTaskItem._customEscapedMetadata); + } + else + { + // If we don't have ITaskItem2 to fall back on, we have to make do with the fact that + // CloneCustomMetadata, GetMetadata, & ItemSpec returns unescaped values, and + // TaskParameterTaskItem's constructor expects escaped values, so escaping them all + // is the closest approximation to correct we can get. + _escapedItemSpec = EscapingUtilities.Escape(copyFrom.ItemSpec); + _escapedDefiningProject = EscapingUtilities.EscapeWithCaching(copyFrom.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)); + + IDictionary customMetadata = copyFrom.CloneCustomMetadata(); + _customEscapedMetadata = new(StringComparer.OrdinalIgnoreCase); + + if (customMetadata?.Count > 0) + { + foreach (DictionaryEntry entry in customMetadata) + { + _customEscapedMetadata[(string)entry.Key] = EscapingUtilities.Escape((string)entry.Value) ?? string.Empty; + } + } + } + + ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec); + } + + private TaskParameterTaskItem() + { + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object? InitializeLifetimeService() => null; + + /// + /// Gets or sets the item "specification" e.g. for disk-based items this would be the file path. + /// + /// + /// This should be named "EvaluatedInclude" but that would be a breaking change to this interface. + /// + /// The item-spec string. + public string ItemSpec + { + get => _escapedItemSpec is null ? string.Empty : EscapingUtilities.UnescapeAll(_escapedItemSpec); + set => _escapedItemSpec = value; + } + + /// + /// Gets the names of all the metadata on the item. + /// Includes the built-in metadata like "FullPath". + /// + /// The list of metadata names. + public ICollection MetadataNames + { + get + { + List result = _customEscapedMetadata is not null + ? [.. _customEscapedMetadata.Keys] + : []; + + result.AddRange(FileUtilities.ItemSpecModifiers.All); + + return result; + } + } + + /// + /// Gets the number of pieces of metadata on the item. Includes + /// both custom and built-in metadata. Used only for unit testing. + /// + /// Count of pieces of metadata. + public int MetadataCount + { + get + { + int count = _customEscapedMetadata?.Count ?? 0; + return count + FileUtilities.ItemSpecModifiers.All.Length; + } + } + + /// + /// Allows the values of metadata on the item to be queried. + /// + /// The name of the metadata to retrieve. + /// The value of the specified metadata. + public string GetMetadata(string metadataName) + { + string metadataValue = GetMetadataValueEscaped(metadataName); + return EscapingUtilities.UnescapeAll(metadataValue); + } + + /// + /// Allows a piece of custom metadata to be set on the item. + /// + /// The name of the metadata to set. + /// The metadata value. + public void SetMetadata(string metadataName, string metadataValue) + { + ErrorUtilities.VerifyThrowArgumentLength(metadataName); + + // Non-derivable metadata can only be set at construction time. + // That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier. + ErrorUtilities.VerifyThrowArgument( + !FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName), + SR.Shared_CannotChangeItemSpecModifiers, + metadataName); + + _customEscapedMetadata ??= new(StringComparer.OrdinalIgnoreCase); + _customEscapedMetadata[metadataName] = metadataValue ?? string.Empty; + } + + /// + /// Allows the removal of custom metadata set on the item. + /// + /// The name of the metadata to remove. + public void RemoveMetadata(string metadataName) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName); + ErrorUtilities.VerifyThrowArgument( + !FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName), + SR.Shared_CannotChangeItemSpecModifiers, + metadataName); + + if (_customEscapedMetadata == null) + { + return; + } + + _customEscapedMetadata.Remove(metadataName); + } + + /// + /// Allows custom metadata on the item to be copied to another item. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should NOT copy over the item-spec. + /// 2) if a particular piece of metadata already exists on the destination item, it should NOT be overwritten. + /// 3) if there are pieces of metadata on the item that make no semantic sense on the destination item, they should NOT be copied. + /// + /// The item to copy metadata to. + public void CopyMetadataTo(ITaskItem destinationItem) + { + ErrorUtilities.VerifyThrowArgumentNull(destinationItem); + + // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata + // between items, and need to know the source item where the metadata came from + string originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec"); + + if (_customEscapedMetadata != null) + { + foreach (KeyValuePair entry in _customEscapedMetadata) + { + string value = destinationItem.GetMetadata(entry.Key); + + if (string.IsNullOrEmpty(value)) + { + destinationItem.SetMetadata(entry.Key, entry.Value); + } + } + } + + if (string.IsNullOrEmpty(originalItemSpec)) + { + destinationItem.SetMetadata("OriginalItemSpec", EscapingUtilities.Escape(ItemSpec)); + } + } + + /// + /// Get the collection of custom metadata. This does not include built-in metadata. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should return a clone of the metadata. + /// 2) writing to this dictionary should not be reflected in the underlying item. + /// + /// Dictionary of cloned metadata. + public IDictionary CloneCustomMetadata() + { + Dictionary clonedMetadata = new(StringComparer.OrdinalIgnoreCase); + + if (_customEscapedMetadata != null) + { + foreach (KeyValuePair metadatum in _customEscapedMetadata) + { + clonedMetadata.Add(metadatum.Key, EscapingUtilities.UnescapeAll(metadatum.Value!)); + } + } + + return clonedMetadata; + } + + private string GetMetadataValueEscaped(string metadataName) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName); + + string? metadataValue = null; + + if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName)) + { + // FileUtilities.GetItemSpecModifier is expecting escaped data, which we assume we already are. + // Passing in a null for currentDirectory indicates we are already in the correct current directory + metadataValue = FileUtilities.ItemSpecModifiers.GetItemSpecModifier( + currentDirectory: null, _escapedItemSpec!, _escapedDefiningProject, metadataName, ref _fullPath); + } + else if (_customEscapedMetadata != null) + { + _customEscapedMetadata.TryGetValue(metadataName, out metadataValue); + } + + return metadataValue ?? string.Empty; + } + + public void Translate(ITranslator translator) + { + translator.Translate(ref _escapedItemSpec); + translator.Translate(ref _escapedDefiningProject); + translator.TranslateDictionary(ref _customEscapedMetadata, StringComparer.OrdinalIgnoreCase); + + ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec); + ErrorUtilities.VerifyThrowInternalNull(_customEscapedMetadata); + } + + internal static TaskParameterTaskItem FactoryForDeserialization(ITranslator translator) + { + var taskItem = new TaskParameterTaskItem(); + taskItem.Translate(translator); + return taskItem; + } + } +} diff --git a/src/MSBuildTaskHost/BackEnd/TaskParameterTypeVerifier.cs b/src/MSBuildTaskHost/BackEnd/TaskParameterTypeVerifier.cs new file mode 100644 index 00000000000..2c5c9989f09 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TaskParameterTypeVerifier.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// Provide a class which can verify the correct type for both input and output parameters. +/// +internal static class TaskParameterTypeVerifier +{ + /// + /// Is the parameter type a valid scalar input value. + /// + internal static bool IsValidScalarInputParameter(Type parameterType) + => parameterType.IsValueType || parameterType == typeof(string) || parameterType == typeof(ITaskItem); + + /// + /// Is the passed in parameterType a valid vector input parameter. + /// + internal static bool IsValidVectorInputParameter(Type parameterType) + => (parameterType.IsArray && parameterType.GetElementType().IsValueType) || + parameterType == typeof(string[]) || + parameterType == typeof(ITaskItem[]); + + /// + /// Is the passed in value type assignable to an ITask or ITask[] object. + /// + internal static bool IsAssignableToITask(Type parameterType) + => typeof(ITaskItem[]).IsAssignableFrom(parameterType) || // ITaskItem array or derived type, or + typeof(ITaskItem).IsAssignableFrom(parameterType); // ITaskItem or derived type + + /// + /// Is the passed parameter a valid value type output parameter. + /// + internal static bool IsValueTypeOutputParameter(Type parameterType) + => (parameterType.IsArray && parameterType.GetElementType().IsValueType) || // array of value types, or + parameterType == typeof(string[]) || // string array, or + parameterType.IsValueType || // value type, or + parameterType == typeof(string); // string + + /// + /// Is the parameter type a valid scalar or value type input parameter. + /// + internal static bool IsValidInputParameter(Type parameterType) + => IsValidScalarInputParameter(parameterType) || IsValidVectorInputParameter(parameterType); + + /// + /// Is the parameter type a valid scalar or value type output parameter. + /// + internal static bool IsValidOutputParameter(Type parameterType) + => IsValueTypeOutputParameter(parameterType) || IsAssignableToITask(parameterType); +} diff --git a/src/MSBuildTaskHost/BackEnd/TranslatorHelpers.cs b/src/MSBuildTaskHost/BackEnd/TranslatorHelpers.cs new file mode 100644 index 00000000000..554306a8583 --- /dev/null +++ b/src/MSBuildTaskHost/BackEnd/TranslatorHelpers.cs @@ -0,0 +1,55 @@ +// 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; + +namespace Microsoft.Build.TaskHost.BackEnd; + +/// +/// This class provides helper methods to adapt from to +/// . +/// +internal static class TranslatorHelpers +{ + /// + /// Translates an object implementing which does not expose a + /// public parameterless constructor. + /// + /// The reference type. + /// The translator. + /// The value to be translated. + /// The factory method used to instantiate values of type T. + public static void Translate( + this ITranslator translator, + ref T instance, + NodePacketValueFactory valueFactory) + where T : class, ITranslatable + { + if (!translator.TranslateNullable(instance)) + { + return; + } + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + instance = valueFactory(translator); + } + else + { + instance.Translate(translator); + } + } + + private static ObjectTranslatorWithValueFactory AdaptFactory(NodePacketValueFactory valueFactory) + where T : class, ITranslatable + => static (ITranslator translator, NodePacketValueFactory valueFactory, ref T objectToTranslate) + => translator.Translate(ref objectToTranslate, valueFactory); + + public static void TranslateDictionary( + this ITranslator translator, + ref Dictionary? dictionary, + IEqualityComparer comparer, + NodePacketValueFactory valueFactory) + where T : class, ITranslatable + => translator.TranslateDictionary(ref dictionary, comparer, AdaptFactory(valueFactory), valueFactory); +} diff --git a/src/MSBuildTaskHost/Collections/CollectionExtensions.cs b/src/MSBuildTaskHost/Collections/CollectionExtensions.cs new file mode 100644 index 00000000000..fd5af45bd37 --- /dev/null +++ b/src/MSBuildTaskHost/Collections/CollectionExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.TaskHost.Collections; + +internal static class CollectionExtensions +{ + /// + /// Checks whether the dictionary contains the specified key with a value that matches the given + /// value using the specified string comparison. + /// + /// The dictionary to check. + /// The key to locate in the dictionary. + /// The value to compare against the dictionary's value. + /// The string comparison method to use. + /// + /// if the dictionary contains the key and its associated value equals + /// the specified value; otherwise, . + /// + internal static bool HasValue( + this Dictionary dictionary, + string key, + string? value, + StringComparison comparison) + => dictionary.TryGetValue(key, out string? existingValue) + && string.Equals(value, existingValue, comparison); +} diff --git a/src/MSBuildTaskHost/Collections/ConcurrentDictionary.cs b/src/MSBuildTaskHost/Collections/ConcurrentDictionary.cs new file mode 100644 index 00000000000..6c17edb64ec --- /dev/null +++ b/src/MSBuildTaskHost/Collections/ConcurrentDictionary.cs @@ -0,0 +1,527 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost.Collections; + +// The following class is back-ported from .NET 4.X CoreFX library because +// MSBuildTaskHost requires 3.5 .NET Framework. Only GetOrAdd method kept. +internal class ConcurrentDictionary + where TKey : notnull +{ + /// + /// Tables that hold the internal state of the ConcurrentDictionary + /// + /// Wrapping the three tables in a single object allows us to atomically + /// replace all tables at once. + /// + private sealed class Tables + { + internal readonly Node[] _buckets; // A singly-linked list for each bucket. + internal readonly object[] _locks; // A set of locks, each guarding a section of the table. + internal volatile int[] _countPerLock; // The number of elements guarded by each lock. + + internal Tables(Node[] buckets, object[] locks, int[] countPerLock) + { + _buckets = buckets; + _locks = locks; + _countPerLock = countPerLock; + } + } + + private volatile Tables _tables; // Internal tables of the dictionary + private readonly IEqualityComparer _comparer; // Key equality comparer + private readonly bool _growLockArray; // Whether to dynamically increase the size of the striped lock + private int _budget; // The maximum number of elements per lock before a resize operation is triggered + + // The default capacity, i.e. the initial # of buckets. When choosing this value, we are making + // a trade-off between the size of a very small dictionary, and the number of resizes when + // constructing a large dictionary. Also, the capacity should not be divisible by a small prime. + private const int DefaultCapacity = 31; + + // The maximum size of the striped lock that will not be exceeded when locks are automatically + // added as the dictionary grows. However, the user is allowed to exceed this limit by passing + // a concurrency level larger than MaxLockNumber into the constructor. + private const int MaxLockNumber = 1024; + + // Whether TValue is a type that can be written atomically (i.e., with no danger of torn reads) + private static readonly bool s_isValueWriteAtomic = IsValueWriteAtomic(); + + /// + /// Determines whether type TValue can be written atomically + /// + private static bool IsValueWriteAtomic() + { + // Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without + // the risk of tearing. + // + // See http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf + Type valueType = typeof(TValue); + if (!valueType.IsValueType) + { + return true; + } + + switch (Type.GetTypeCode(valueType)) + { + case TypeCode.Boolean: + case TypeCode.Byte: + case TypeCode.Char: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.SByte: + case TypeCode.Single: + case TypeCode.UInt16: + case TypeCode.UInt32: + return true; + case TypeCode.Int64: + case TypeCode.Double: + case TypeCode.UInt64: + return IntPtr.Size == 8; + default: + return false; + } + } + + /// + /// Initializes a new instance of the + /// class that is empty, has the default concurrency level, has the default initial capacity, and + /// uses the default comparer for the key type. + /// + public ConcurrentDictionary(IEqualityComparer? comparer = null) + { + int concurrencyLevel = NativeMethods.GetLogicalCoreCount(); + int capacity = DefaultCapacity; + + // The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard + // any buckets. + if (capacity < concurrencyLevel) + { + capacity = concurrencyLevel; + } + + object[] locks = new object[concurrencyLevel]; + for (int i = 0; i < locks.Length; i++) + { + locks[i] = new object(); + } + + int[] countPerLock = new int[locks.Length]; + Node[] buckets = new Node[capacity]; + _tables = new Tables(buckets, locks, countPerLock); + + _comparer = comparer ?? EqualityComparer.Default; + _growLockArray = true; + _budget = buckets.Length / locks.Length; + } + + private bool TryGetValueInternal(TKey key, int hashcode, [MaybeNullWhen(false)] out TValue value) + { + Debug.Assert(_comparer.GetHashCode(key) == hashcode); + + // We must capture the _buckets field in a local variable. It is set to a new table on each table resize. + Tables tables = _tables; + + int bucketNo = GetBucket(hashcode, tables._buckets.Length); + + // We can get away w/out a lock here. + // The Volatile.Read ensures that we have a copy of the reference to tables._buckets[bucketNo]. + // This protects us from reading fields ('_hashcode', '_key', '_value' and '_next') of different instances. + Thread.MemoryBarrier(); + Node? n = tables._buckets[bucketNo]; + + while (n != null) + { + if (hashcode == n._hashcode && _comparer.Equals(n._key, key)) + { + value = n._value; + return true; + } + + n = n._next; + } + + value = default; + return false; + } + + /// + /// Shared internal implementation for inserts and updates. + /// If key exists, we always return false; and if updateIfExists == true we force update with value; + /// If key doesn't exist, we always add value and return true; + /// + private bool TryAddInternal(TKey key, int hashcode, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue) + { + Debug.Assert(_comparer.GetHashCode(key) == hashcode); + + while (true) + { + int bucketNo, lockNo; + + Tables tables = _tables; + GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + + bool resizeDesired = false; + bool lockTaken = false; + try + { + if (acquireLock) + { + lockTaken = Monitor.TryEnter(tables._locks[lockNo]); + } + + // If the table just got resized, we may not be holding the right lock, and must retry. + // This should be a rare occurrence. + if (tables != _tables) + { + continue; + } + + // Try to find this key in the bucket + Node? prev = null; + for (Node? node = tables._buckets[bucketNo]; node != null; node = node._next) + { + Debug.Assert((prev == null && node == tables._buckets[bucketNo]) || prev!._next == node); + if (hashcode == node._hashcode && _comparer.Equals(node._key, key)) + { + // The key was found in the dictionary. If updates are allowed, update the value for that key. + // We need to create a new node for the update, in order to support TValue types that cannot + // be written atomically, since lock-free reads may be happening concurrently. + if (updateIfExists) + { + if (s_isValueWriteAtomic) + { + node._value = value; + } + else + { + Node newNode = new Node(node._key, value, hashcode, node._next); + if (prev == null) + { + Interlocked.Exchange(ref tables._buckets[bucketNo], newNode); + } + else + { + prev._next = newNode; + } + } + resultingValue = value; + } + else + { + resultingValue = node._value; + } + return false; + } + prev = node; + } + + // The key was not found in the bucket. Insert the key-value pair. + Interlocked.Exchange(ref tables._buckets[bucketNo], new Node(key, value, hashcode, tables._buckets[bucketNo])); + checked + { + tables._countPerLock[lockNo]++; + } + + // If the number of elements guarded by this lock has exceeded the budget, resize the bucket table. + // It is also possible that GrowTable will increase the budget but won't resize the bucket table. + // That happens if the bucket table is found to be poorly utilized due to a bad hash function. + if (tables._countPerLock[lockNo] > _budget) + { + resizeDesired = true; + } + } + finally + { + if (lockTaken) + { + Monitor.Exit(tables._locks[lockNo]); + } + } + + // The fact that we got here means that we just performed an insertion. If necessary, we will grow the table. + // + // Concurrency notes: + // - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks. + // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0 + // and then verify that the table we passed to it as the argument is still the current table. + if (resizeDesired) + { + GrowTable(tables); + } + + resultingValue = value; + return true; + } + } + + [DoesNotReturn] + private static void ThrowKeyNullException() + => throw new ArgumentNullException("key"); + + /// + /// Adds a key/value pair to the + /// if the key does not already exist. + /// + /// The key of the element to add. + /// The function used to generate a value for the key. + /// is a null reference + /// (Nothing in Visual Basic). + /// is a null reference + /// (Nothing in Visual Basic). + /// The dictionary contains too many + /// elements. + /// The value for the key. This will be either the existing value for the key if the + /// key is already in the dictionary, or the new value for the key as returned by valueFactory + /// if the key was not in the dictionary. + public TValue GetOrAdd(TKey key, Func valueFactory) + { + if (key == null) + { + ThrowKeyNullException(); + } + + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } + + int hashcode = _comparer.GetHashCode(key); + + if (!TryGetValueInternal(key, hashcode, out TValue? resultingValue)) + { + TryAddInternal(key, hashcode, valueFactory(key), updateIfExists: false, acquireLock: true, out resultingValue); + } + + return resultingValue; + } + + /// + /// Replaces the bucket table with a larger one. To prevent multiple threads from resizing the + /// table as a result of races, the Tables instance that holds the table of buckets deemed too + /// small is passed in as an argument to GrowTable(). GrowTable() obtains a lock, and then checks + /// the Tables instance has been replaced in the meantime or not. + /// + private void GrowTable(Tables tables) + { + const int MaxArrayLength = 0X7FEFFFFF; + int locksAcquired = 0; + try + { + // The thread that first obtains _locks[0] will be the one doing the resize operation + AcquireLocks(0, 1, ref locksAcquired); + + // Make sure nobody resized the table while we were waiting for lock 0: + if (tables != _tables) + { + // We assume that since the table reference is different, it was already resized (or the budget + // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons, + // we will have to revisit this logic. + return; + } + + // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow. + long approxCount = 0; + for (int i = 0; i < tables._countPerLock.Length; i++) + { + approxCount += tables._countPerLock[i]; + } + + // If the bucket array is too empty, double the budget instead of resizing the table + if (approxCount < tables._buckets.Length / 4) + { + _budget = 2 * _budget; + + if (_budget < 0) + { + _budget = int.MaxValue; + } + + return; + } + + // Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by + // 2,3,5 or 7. We can consider a different table-sizing policy in the future. + int newLength = 0; + bool maximizeTableSize = false; + try + { + checked + { + // Double the size of the buckets table and add one, so that we have an odd integer. + newLength = (tables._buckets.Length * 2) + 1; + + // Now, we only need to check odd integers, and find the first that is not divisible + // by 3, 5 or 7. + while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) + { + newLength += 2; + } + + Debug.Assert(newLength % 2 != 0); + + if (newLength > MaxArrayLength) + { + maximizeTableSize = true; + } + } + } + catch (OverflowException) + { + maximizeTableSize = true; + } + + if (maximizeTableSize) + { + newLength = MaxArrayLength; + + // We want to make sure that GrowTable will not be called again, since table is at the maximum size. + // To achieve that, we set the budget to int.MaxValue. + // + // (There is one special case that would allow GrowTable() to be called in the future: + // calling Clear() on the ConcurrentDictionary will shrink the table and lower the budget.) + _budget = int.MaxValue; + } + + // Now acquire all other locks for the table + AcquireLocks(1, tables._locks.Length, ref locksAcquired); + + object[] newLocks = tables._locks; + + // Add more locks + if (_growLockArray && tables._locks.Length < MaxLockNumber) + { + newLocks = new object[tables._locks.Length * 2]; + Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); + for (int i = tables._locks.Length; i < newLocks.Length; i++) + { + newLocks[i] = new object(); + } + } + + Node[] newBuckets = new Node[newLength]; + int[] newCountPerLock = new int[newLocks.Length]; + + // Copy all data into a new table, creating new nodes for all elements + for (int i = 0; i < tables._buckets.Length; i++) + { + Node? current = tables._buckets[i]; + while (current != null) + { + Node? next = current._next; + int newBucketNo, newLockNo; + GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length); + + newBuckets[newBucketNo] = new Node(current._key, current._value, current._hashcode, newBuckets[newBucketNo]); + + checked + { + newCountPerLock[newLockNo]++; + } + + current = next; + } + } + + // Adjust the budget + _budget = Math.Max(1, newBuckets.Length / newLocks.Length); + + // Replace tables with the new versions + _tables = new Tables(newBuckets, newLocks, newCountPerLock); + } + finally + { + // Release all locks that we took earlier + ReleaseLocks(0, locksAcquired); + } + } + + /// + /// Computes the bucket for a particular key. + /// + private static int GetBucket(int hashcode, int bucketCount) + { + int bucketNo = (hashcode & 0x7fffffff) % bucketCount; + Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); + + return bucketNo; + } + + /// + /// Computes the bucket and lock number for a particular key. + /// + private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) + { + bucketNo = (hashcode & 0x7fffffff) % bucketCount; + lockNo = bucketNo % lockCount; + + Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); + Debug.Assert(lockNo >= 0 && lockNo < lockCount); + } + + /// + /// Acquires a contiguous range of locks for this hash table, and increments locksAcquired + /// by the number of locks that were successfully acquired. The locks are acquired in an + /// increasing order. + /// + private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) + { + Debug.Assert(fromInclusive <= toExclusive); + object[] locks = _tables._locks; + + for (int i = fromInclusive; i < toExclusive; i++) + { + bool lockTaken = false; + try + { + lockTaken = Monitor.TryEnter(locks[i]); + } + finally + { + if (lockTaken) + { + locksAcquired++; + } + } + } + } + + /// + /// Releases a contiguous range of locks. + /// + private void ReleaseLocks(int fromInclusive, int toExclusive) + { + Debug.Assert(fromInclusive <= toExclusive); + + for (int i = fromInclusive; i < toExclusive; i++) + { + Monitor.Exit(_tables._locks[i]); + } + } + + /// + /// A node in a singly-linked list representing a particular hash table bucket. + /// + private sealed class Node + { + internal readonly TKey _key; + internal TValue _value; + internal volatile Node? _next; + internal readonly int _hashcode; + + internal Node(TKey key, TValue value, int hashcode, Node? next) + { + _key = key; + _value = value; + _next = next; + _hashcode = hashcode; + } + } +} diff --git a/src/MSBuildTaskHost/Collections/ConcurrentQueue.cs b/src/MSBuildTaskHost/Collections/ConcurrentQueue.cs new file mode 100644 index 00000000000..140c4c767e2 --- /dev/null +++ b/src/MSBuildTaskHost/Collections/ConcurrentQueue.cs @@ -0,0 +1,571 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Microsoft.Build.TaskHost.Collections; + +// The following class is back-ported from .NET 4.X CoreFX library because +// MSBuildTaskHost requires 3.5 .NET Framework. Only important methods (Enqueue, TryDequeue) are kept. +internal class ConcurrentQueue +{ + // This implementation provides an unbounded, multi-producer multi-consumer queue + // that supports the standard Enqueue/TryDequeue operations, as well as support for + // snapshot enumeration (GetEnumerator, ToArray, CopyTo), peeking, and Count/IsEmpty. + // It is composed of a linked list of bounded ring buffers, each of which has a head + // and a tail index, isolated from each other to minimize false sharing. As long as + // the number of elements in the queue remains less than the size of the current + // buffer (Segment), no additional allocations are required for enqueued items. When + // the number of items exceeds the size of the current segment, the current segment is + // "frozen" to prevent further enqueues, and a new segment is linked from it and set + // as the new tail segment for subsequent enqueues. As old segments are consumed by + // dequeues, the head reference is updated to point to the segment that dequeuers should + // try next. To support snapshot enumeration, segments also support the notion of + // preserving for observation, whereby they avoid overwriting state as part of dequeues. + // Any operation that requires a snapshot results in all current segments being + // both frozen for enqueues and preserved for observation: any new enqueues will go + // to new segments, and dequeuers will consume from the existing segments but without + // overwriting the existing data. + + /// Initial length of the segments used in the queue. + private const int InitialSegmentLength = 32; + + /// + /// Maximum length of the segments used in the queue. This is a somewhat arbitrary limit: + /// larger means that as long as we don't exceed the size, we avoid allocating more segments, + /// but if we do exceed it, then the segment becomes garbage. + /// + private const int MaxSegmentLength = 1024 * 1024; + + /// + /// Lock used to protect cross-segment operations, including any updates to or + /// and any operations that need to get a consistent view of them. + /// + private readonly object _crossSegmentLock; + + /// The current tail segment. + private volatile Segment _tail; + + /// The current head segment. + private volatile Segment _head; + + internal static object VolatileReader(ref object o) => Thread.VolatileRead(ref o); + + /// + /// Initializes a new instance of the class. + /// + public ConcurrentQueue() + { + _crossSegmentLock = new object(); + _tail = _head = new Segment(InitialSegmentLength); + } + + /// Adds an object to the end of the . + /// + /// The object to add to the end of the . + /// The value can be a null reference (Nothing in Visual Basic) for reference types. + /// + public void Enqueue(T item) + { + // Try to enqueue to the current tail. + if (!_tail.TryEnqueue(item)) + { + // If we're unable to, we need to take a slow path that will + // try to add a new tail segment. + EnqueueSlow(item); + } + } + + /// Adds to the end of the queue, adding a new segment if necessary. + private void EnqueueSlow(T item) + { + while (true) + { + Segment tail = _tail; + + // Try to append to the existing tail. + if (tail.TryEnqueue(item)) + { + return; + } + + // If we were unsuccessful, take the lock so that we can compare and manipulate + // the tail. Assuming another enqueuer hasn't already added a new segment, + // do so, then loop around to try enqueueing again. + lock (_crossSegmentLock) + { + if (tail == _tail) + { + // Make sure no one else can enqueue to this segment. + tail.EnsureFrozenForEnqueues(); + + // We determine the new segment's length based on the old length. + // In general, we double the size of the segment, to make it less likely + // that we'll need to grow again. However, if the tail segment is marked + // as preserved for observation, something caused us to avoid reusing this + // segment, and if that happens a lot and we grow, we'll end up allocating + // lots of wasted space. As such, in such situations we reset back to the + // initial segment length; if these observations are happening frequently, + // this will help to avoid wasted memory, and if they're not, we'll + // relatively quickly grow again to a larger size. + int nextSize = tail._preservedForObservation != 0 ? InitialSegmentLength : Math.Min(tail.Capacity * 2, MaxSegmentLength); + var newTail = new Segment(nextSize); + + // Hook up the new tail. + tail._nextSegment = newTail; + _tail = newTail; + } + } + } + } + + /// + /// Attempts to remove and return the object at the beginning of the . + /// + /// + /// When this method returns, if the operation was successful, contains the + /// object removed. If no object was available to be removed, the value is unspecified. + /// + /// + /// true if an element was removed and returned from the beginning of the + /// successfully; otherwise, false. + /// + public bool TryDequeue([MaybeNullWhen(false)] out T result) => + _head.TryDequeue(out result) || // fast-path that operates just on the head segment + TryDequeueSlow(out result); // slow path that needs to fix up segments + + /// Tries to dequeue an item, removing empty segments as needed. + private bool TryDequeueSlow([MaybeNullWhen(false)] out T item) + { + while (true) + { + // Get the current head + Segment head = _head; + + // Try to take. If we're successful, we're done. + if (head.TryDequeue(out item)) + { + return true; + } + + // Check to see whether this segment is the last. If it is, we can consider + // this to be a moment-in-time empty condition (even though between the TryDequeue + // check and this check, another item could have arrived). + if (head._nextSegment == null) + { + item = default; + return false; + } + + // At this point we know that head.Next != null, which means + // this segment has been frozen for additional enqueues. But between + // the time that we ran TryDequeue and checked for a next segment, + // another item could have been added. Try to dequeue one more time + // to confirm that the segment is indeed empty. + Debug.Assert(head._frozenForEnqueues); + if (head.TryDequeue(out item)) + { + return true; + } + + // This segment is frozen (nothing more can be added) and empty (nothing is in it). + // Update head to point to the next segment in the list, assuming no one's beat us to it. + lock (_crossSegmentLock) + { + if (head == _head) + { + _head = head._nextSegment; + } + } + } + } + + /// + /// Attempts to return an object from the beginning of the + /// without removing it. + /// + /// + /// When this method returns, contains an object from + /// the beginning of the or default(T) + /// if the operation failed. + /// + /// true if and object was returned successfully; otherwise, false. + /// + /// For determining whether the collection contains any items, use of the + /// property is recommended rather than peeking. + /// + public bool TryPeek([MaybeNullWhen(false)] out T result) => TryPeek(out result, resultUsed: true); + + /// Attempts to retrieve the value for the first element in the queue. + /// The value of the first element, if found. + /// true if the result is neede; otherwise false if only the true/false outcome is needed. + /// true if an element was found; otherwise, false. + private bool TryPeek([MaybeNullWhen(false)] out T result, bool resultUsed) + { + // Starting with the head segment, look through all of the segments + // for the first one we can find that's not empty. + Segment s = _head; + while (true) + { + // Grab the next segment from this one, before we peek. + // This is to be able to see whether the value has changed + // during the peek operation. + Thread.MemoryBarrier(); + Segment? next = s._nextSegment; + + // Peek at the segment. If we find an element, we're done. + if (s.TryPeek(out result, resultUsed)) + { + return true; + } + + // The current segment was empty at the moment we checked. + + if (next != null) + { + // If prior to the peek there was already a next segment, then + // during the peek no additional items could have been enqueued + // to it and we can just move on to check the next segment. + Debug.Assert(next == s._nextSegment); + s = next; + } + else + { + Thread.MemoryBarrier(); + if (s._nextSegment == null) + { + // The next segment is null. Nothing more to peek at. + break; + } + } + + // The next segment was null before we peeked but non-null after. + // That means either when we peeked the first segment had + // already been frozen but the new segment not yet added, + // or that the first segment was empty and between the time + // that we peeked and then checked _nextSegment, so many items + // were enqueued that we filled the first segment and went + // into the next. Since we need to peek in order, we simply + // loop around again to peek on the same segment. The next + // time around on this segment we'll then either successfully + // peek or we'll find that next was non-null before peeking, + // and we'll traverse to that segment. + } + + result = default; + return false; + } + + /// + /// Provides a multi-producer, multi-consumer thread-safe bounded segment. When the queue is full, + /// enqueues fail and return false. When the queue is empty, dequeues fail and return null. + /// These segments are linked together to form the unbounded . + /// + [DebuggerDisplay("Capacity = {Capacity}")] + private sealed class Segment + { + // Segment design is inspired by the algorithm outlined at: + // http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue + + /// The array of items in this queue. Each slot contains the item in that slot and its "sequence number". + internal readonly Slot[] _slots; + /// Mask for quickly accessing a position within the queue's array. + internal readonly int _slotsMask; + /// The head and tail positions, with padding to help avoid false sharing contention. + /// Dequeuing happens from the head, enqueuing happens at the tail. + internal PaddedHeadAndTail _headAndTail; // mutable struct: do not make this readonly + + /// Indicates whether the segment has been marked such that dequeues don't overwrite the removed data. + internal byte _preservedForObservation; + /// Indicates whether the segment has been marked such that no additional items may be enqueued. + internal bool _frozenForEnqueues; + /// The segment following this one in the queue, or null if this segment is the last in the queue. + internal Segment? _nextSegment; + + /// Creates the segment. + /// + /// The maximum number of elements the segment can contain. Must be a power of 2. + /// + public Segment(int boundedLength) + { + // Validate the length + Debug.Assert(boundedLength >= 2, $"Must be >= 2, got {boundedLength}"); + Debug.Assert((boundedLength & (boundedLength - 1)) == 0, $"Must be a power of 2, got {boundedLength}"); + + // Initialize the slots and the mask. The mask is used as a way of quickly doing "% _slots.Length", + // instead letting us do "& _slotsMask". + _slots = new Slot[boundedLength]; + _slotsMask = boundedLength - 1; + + // Initialize the sequence number for each slot. The sequence number provides a ticket that + // allows dequeuers to know whether they can dequeue and enqueuers to know whether they can + // enqueue. An enqueuer at position N can enqueue when the sequence number is N, and a dequeuer + // for position N can dequeue when the sequence number is N + 1. When an enqueuer is done writing + // at position N, it sets the sequence number to N + 1 so that a dequeuer will be able to dequeue, + // and when a dequeuer is done dequeueing at position N, it sets the sequence number to N + _slots.Length, + // so that when an enqueuer loops around the slots, it'll find that the sequence number at + // position N is N. This also means that when an enqueuer finds that at position N the sequence + // number is < N, there is still a value in that slot, i.e. the segment is full, and when a + // dequeuer finds that the value in a slot is < N + 1, there is nothing currently available to + // dequeue. (It is possible for multiple enqueuers to enqueue concurrently, writing into + // subsequent slots, and to have the first enqueuer take longer, so that the slots for 1, 2, 3, etc. + // may have values, but the 0th slot may still be being filled... in that case, TryDequeue will + // return false.) + for (int i = 0; i < _slots.Length; i++) + { + _slots[i].SequenceNumber = i; + } + } + + /// Gets the number of elements this segment can store. + internal int Capacity => _slots.Length; + + /// Gets the "freeze offset" for this segment. + internal int FreezeOffset => _slots.Length * 2; + + /// + /// Ensures that the segment will not accept any subsequent enqueues that aren't already underway. + /// + /// + /// When we mark a segment as being frozen for additional enqueues, + /// we set the bool, but that's mostly + /// as a small helper to avoid marking it twice. The real marking comes + /// by modifying the Tail for the segment, increasing it by this + /// . This effectively knocks it off the + /// sequence expected by future enqueuers, such that any additional enqueuer + /// will be unable to enqueue due to it not lining up with the expected + /// sequence numbers. This value is chosen specially so that Tail will grow + /// to a value that maps to the same slot but that won't be confused with + /// any other enqueue/dequeue sequence number. + /// + internal void EnsureFrozenForEnqueues() // must only be called while queue's segment lock is held + { + if (!_frozenForEnqueues) // flag used to ensure we don't increase the Tail more than once if frozen more than once + { + _frozenForEnqueues = true; + + // Increase the tail by FreezeOffset, spinning until we're successful in doing so. + while (true) + { + int tail = Thread.VolatileRead(ref _headAndTail.Tail); + if (Interlocked.CompareExchange(ref _headAndTail.Tail, tail + FreezeOffset, tail) == tail) + { + break; + } + Thread.SpinWait(1); + } + } + } + + /// Tries to dequeue an element from the queue. + public bool TryDequeue([MaybeNullWhen(false)] out T item) + { + // Loop in case of contention... + while (true) + { + // Get the head at which to try to dequeue. + int currentHead = Thread.VolatileRead(ref _headAndTail.Head); + int slotsIndex = currentHead & _slotsMask; + + // Read the sequence number for the head position. + int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); + + // We can dequeue from this slot if it's been filled by an enqueuer, which + // would have left the sequence number at pos+1. + int diff = sequenceNumber - (currentHead + 1); + if (diff == 0) + { + // We may be racing with other dequeuers. Try to reserve the slot by incrementing + // the head. Once we've done that, no one else will be able to read from this slot, + // and no enqueuer will be able to read from this slot until we've written the new + // sequence number. WARNING: The next few lines are not reliable on a runtime that + // supports thread aborts. If a thread abort were to sneak in after the CompareExchange + // but before the Volatile.Write, enqueuers trying to enqueue into this slot would + // spin indefinitely. If this implementation is ever used on such a platform, this + // if block should be wrapped in a finally / prepared region. + if (Interlocked.CompareExchange(ref _headAndTail.Head, currentHead + 1, currentHead) == currentHead) + { + // Successfully reserved the slot. Note that after the above CompareExchange, other threads + // trying to dequeue from this slot will end up spinning until we do the subsequent Write. + item = _slots[slotsIndex].Item!; + if (Thread.VolatileRead(ref _preservedForObservation) == 0) + { + // If we're preserving, though, we don't zero out the slot, as we need it for + // enumerations, peeking, ToArray, etc. And we don't update the sequence number, + // so that an enqueuer will see it as full and be forced to move to a new segment. + _slots[slotsIndex].Item = default; + Thread.VolatileWrite(ref _slots[slotsIndex].SequenceNumber, currentHead + _slots.Length); + } + + return true; + } + } + else if (diff < 0) + { + // The sequence number was less than what we needed, which means this slot doesn't + // yet contain a value we can dequeue, i.e. the segment is empty. Technically it's + // possible that multiple enqueuers could have written concurrently, with those + // getting later slots actually finishing first, so there could be elements after + // this one that are available, but we need to dequeue in order. So before declaring + // failure and that the segment is empty, we check the tail to see if we're actually + // empty or if we're just waiting for items in flight or after this one to become available. + bool frozen = _frozenForEnqueues; + int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); + if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0))) + { + item = default; + return false; + } + + // It's possible it could have become frozen after we checked _frozenForEnqueues + // and before reading the tail. That's ok: in that rare race condition, we just + // loop around again. + } + + // Lost a race. Spin a bit, then try again. + Thread.SpinWait(1); + } + } + + /// Tries to peek at an element from the queue, without removing it. + public bool TryPeek([MaybeNullWhen(false)] out T result, bool resultUsed) + { + if (resultUsed) + { + // In order to ensure we don't get a torn read on the value, we mark the segment + // as preserving for observation. Additional items can still be enqueued to this + // segment, but no space will be freed during dequeues, such that the segment will + // no longer be reusable. + _preservedForObservation = 1; + Thread.MemoryBarrier(); + } + + // Loop in case of contention... + while (true) + { + // Get the head at which to try to peek. + int currentHead = Thread.VolatileRead(ref _headAndTail.Head); + int slotsIndex = currentHead & _slotsMask; + + // Read the sequence number for the head position. + int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); + + // We can peek from this slot if it's been filled by an enqueuer, which + // would have left the sequence number at pos+1. + int diff = sequenceNumber - (currentHead + 1); + if (diff == 0) + { + result = resultUsed ? _slots[slotsIndex].Item! : default!; + return true; + } + else if (diff < 0) + { + // The sequence number was less than what we needed, which means this slot doesn't + // yet contain a value we can peek, i.e. the segment is empty. Technically it's + // possible that multiple enqueuers could have written concurrently, with those + // getting later slots actually finishing first, so there could be elements after + // this one that are available, but we need to peek in order. So before declaring + // failure and that the segment is empty, we check the tail to see if we're actually + // empty or if we're just waiting for items in flight or after this one to become available. + bool frozen = _frozenForEnqueues; + int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); + if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0))) + { + result = default; + return false; + } + + // It's possible it could have become frozen after we checked _frozenForEnqueues + // and before reading the tail. That's ok: in that rare race condition, we just + // loop around again. + } + + // Lost a race. Spin a bit, then try again. + Thread.SpinWait(1); + } + } + + /// + /// Attempts to enqueue the item. If successful, the item will be stored + /// in the queue and true will be returned; otherwise, the item won't be stored, and false + /// will be returned. + /// + public bool TryEnqueue(T item) + { + // Loop in case of contention... + while (true) + { + // Get the tail at which to try to return. + int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); + int slotsIndex = currentTail & _slotsMask; + + // Read the sequence number for the tail position. + int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); + + // The slot is empty and ready for us to enqueue into it if its sequence + // number matches the slot. + int diff = sequenceNumber - currentTail; + if (diff == 0) + { + // We may be racing with other enqueuers. Try to reserve the slot by incrementing + // the tail. Once we've done that, no one else will be able to write to this slot, + // and no dequeuer will be able to read from this slot until we've written the new + // sequence number. WARNING: The next few lines are not reliable on a runtime that + // supports thread aborts. If a thread abort were to sneak in after the CompareExchange + // but before the Volatile.Write, other threads will spin trying to access this slot. + // If this implementation is ever used on such a platform, this if block should be + // wrapped in a finally / prepared region. + if (Interlocked.CompareExchange(ref _headAndTail.Tail, currentTail + 1, currentTail) == currentTail) + { + // Successfully reserved the slot. Note that after the above CompareExchange, other threads + // trying to return will end up spinning until we do the subsequent Write. + _slots[slotsIndex].Item = item; + Thread.VolatileWrite(ref _slots[slotsIndex].SequenceNumber, currentTail + 1); + return true; + } + } + else if (diff < 0) + { + // The sequence number was less than what we needed, which means this slot still + // contains a value, i.e. the segment is full. Technically it's possible that multiple + // dequeuers could have read concurrently, with those getting later slots actually + // finishing first, so there could be spaces after this one that are available, but + // we need to enqueue in order. + return false; + } + + // Lost a race. Spin a bit, then try again. + Thread.SpinWait(1); + } + } + + /// Represents a slot in the queue. + [StructLayout(LayoutKind.Auto)] + [DebuggerDisplay("Item = {Item}, SequenceNumber = {SequenceNumber}")] + internal struct Slot + { + /// The item. + public T? Item; + + /// The sequence number for this slot, used to synchronize between enqueuers and dequeuers. + public int SequenceNumber; + } + } +} + +/// Padded head and tail indices, to avoid false sharing between producers and consumers. +[DebuggerDisplay("Head = {Head}, Tail = {Tail}")] +[StructLayout(LayoutKind.Explicit, Size = 192)] // padding before/between/after fields based on typical cache line size of 64 +internal struct PaddedHeadAndTail +{ + [FieldOffset(64)] + public int Head; + + [FieldOffset(128)] + public int Tail; +} diff --git a/src/MSBuildTaskHost/CommunicationsUtilities.cs b/src/MSBuildTaskHost/CommunicationsUtilities.cs new file mode 100644 index 00000000000..0ffec614712 --- /dev/null +++ b/src/MSBuildTaskHost/CommunicationsUtilities.cs @@ -0,0 +1,767 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Pipes; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; +using Microsoft.Build.TaskHost.BackEnd; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost; + +/// +/// Enumeration of all possible (currently supported) options for handshakes. +/// +[Flags] +internal enum HandshakeOptions +{ + None = 0, + + /// + /// Process is a TaskHost. + /// + TaskHost = 1, + + /// + /// Using the 2.0 CLR. + /// + CLR2 = 2, + + /// + /// 64-bit Intel process. + /// + X64 = 4, + + /// + /// Node reuse enabled. + /// + NodeReuse = 8, + + /// + /// Building with BelowNormal priority. + /// + LowPriority = 16, + + /// + /// Building with administrator privileges. + /// + Administrator = 32, + + /// + /// Using the .NET Core/.NET 5.0+ runtime. + /// + NET = 64, + + /// + /// ARM64 process. + /// + Arm64 = 128, + + /// + /// Using a long-running sidecar TaskHost process to reduce startup overhead and reuse in-memory caches. + /// + SidecarTaskHost = 256, +} + +/// +/// Status codes for the handshake process. +/// It aggregates return values across several functions so we use an aggregate instead of a separate class for each method. +/// +internal enum HandshakeStatus +{ + /// + /// The handshake operation completed successfully. + /// + Success = 0, + + /// + /// The other node returned a different value than expected. + /// This can happen either by attempting to connect to a wrong node type + /// (e.g., transient TaskHost trying to connect to a long-running TaskHost) + /// or by trying to connect to a node that has a different MSBuild version. + /// + VersionMismatch = 1, + + /// + /// The handshake was aborted due to connection from an old MSBuild version. + /// Occurs in TryReadInt when detecting legacy MSBuild.exe connections. + /// + OldMSBuild = 2, + + /// + /// The handshake operation timed out before completion. + /// + Timeout = 3, + + /// + /// The stream ended unexpectedly during the handshake operation. + /// Indicates an incomplete or corrupted handshake sequence. + /// + UnexpectedEndOfStream = 4, + + /// + /// The endianness (byte order) of the communicating nodes does not match. + /// Indicates an architecture compatibility issue. + /// + EndiannessMismatch = 5, + + /// + /// The handshake status is undefined or uninitialized. + /// + Undefined, +} + +/// +/// An aggregate class for passing around results of a handshake and adjacent information. +/// ErrorMessage is to propagate error messages where necessary. +/// +internal sealed class HandshakeResult +{ + /// + /// Gets the status code indicating the result of the handshake operation. + /// + public HandshakeStatus Status { get; } + + /// + /// Handshake in MSBuild is performed as passing integers back and forth. + /// This field holds the value returned from a successful handshake step. + /// + public int Value { get; } + + /// + /// Gets the error message when a handshake operation fails. + /// + public string? ErrorMessage { get; } + + /// + /// The negotiated packet version with the child node. + /// It's needed to ensure both sides of the communication can read/write data in pipe. + /// + public byte NegotiatedPacketVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The status of the handshake operation. + /// The value returned from the handshake. + /// The error message if the handshake failed. + /// The packet version from the child node. + private HandshakeResult(HandshakeStatus status, int value, string? errorMessage, byte negotiatedPacketVersion = 1) + { + Status = status; + Value = value; + ErrorMessage = errorMessage; + NegotiatedPacketVersion = negotiatedPacketVersion; + } + + /// + /// Creates a successful handshake result with the specified value. + /// + /// The value returned from the handshake operation. + /// The packet version received from the child node. + /// A new instance representing a successful operation. + public static HandshakeResult Success(int value = 0, byte negotiatedPacketVersion = 1) + => new(HandshakeStatus.Success, value, null, negotiatedPacketVersion); + + /// + /// Creates a failed handshake result with the specified status and error message. + /// + /// The error status code for the failure. + /// A description of the error that occurred. + /// A new instance representing a failed operation. + public static HandshakeResult Failure(HandshakeStatus status, string errorMessage) + => new(status, value: 0, errorMessage); +} + +internal sealed class Handshake +{ + /// + /// Marker indicating that the next integer in the child handshake response is the PacketVersion. + /// + public const int PacketVersionFromChildMarker = -1; + + private readonly HandshakeComponents _handshakeComponents; + + // Helper method to validate handshake option presence + internal static bool IsHandshakeOptionEnabled(HandshakeOptions hostContext, HandshakeOptions option) + => (hostContext & option) == option; + + // Source options of the handshake. + internal HandshakeOptions HandshakeOptions { get; } + + public Handshake(HandshakeOptions nodeType) + { + HandshakeOptions = nodeType; + + // Build handshake options with version in upper bits + const int handshakeVersion = CommunicationsUtilities.HandshakeVersion; + int options = (int)nodeType | (handshakeVersion << 24); + CommunicationsUtilities.Trace($"Building handshake for node type {nodeType}, (version {handshakeVersion}): options {options}."); + + string toolsDirectory = FileUtilities.MSBuildTaskHostDirectory; + + // Calculate salt from environment and tools directory + string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? ""; + int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}"); + + CommunicationsUtilities.Trace($"Handshake salt is {handshakeSalt}"); + CommunicationsUtilities.Trace($"Tools directory root is {toolsDirectory}"); + + int sessionId = EnvironmentUtilities.ProcessSessionId; + + _handshakeComponents = CreateStandardComponents(options, salt, sessionId); + } + + private static HandshakeComponents CreateStandardComponents(int options, int salt, int sessionId) + { + var fileVersion = new Version(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion); + + return new( + options, + salt, + fileVersion.Major, + fileVersion.Minor, + fileVersion.Build, + fileVersion.Revision, + sessionId); + } + + public HandshakeComponents RetrieveHandshakeComponents() => new( + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.Options), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.Salt), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionMajor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionMinor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionBuild), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionPrivate), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.SessionId)); +} + +/// +/// This class contains utility methods for the MSBuild engine. +/// +internal static class CommunicationsUtilities +{ + /// + /// Indicates to the NodeEndpoint that all the various parts of the Handshake have been sent. + /// + private const int EndOfHandshakeSignal = -0x2a2a2a2a; + + /// + /// The version of the handshake. This should be updated each time the handshake structure is altered. + /// + internal const byte HandshakeVersion = 0x01; + + /// + /// The timeout to connect to a node. + /// + private const int DefaultNodeConnectionTimeout = 900 * 1000; // 15 minutes; enough time that a dev will typically do another build in this time + + /// + /// Whether to trace communications. + /// + private static readonly bool s_trace = Traits.Instance.DebugNodeCommunication; + + /// + /// Lock trace to ensure we are logging in serial fashion. + /// + private static readonly object s_traceLock = new(); + + /// + /// Place to dump trace. + /// + private static string? s_debugDumpPath; + + /// + /// Ticks at last time logged. + /// + private static long s_lastLoggedTicks = DateTime.UtcNow.Ticks; + + /// + /// Gets or sets the node connection timeout. + /// + internal static int NodeConnectionTimeout + => GetIntegerVariableOrDefault("MSBUILDNODECONNECTIONTIMEOUT", DefaultNodeConnectionTimeout); + + /// + /// Get environment block. + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern unsafe char* GetEnvironmentStrings(); + + /// + /// Free environment block. + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern unsafe bool FreeEnvironmentStrings(char* pStrings); + + /// + /// Set environment variable P/Invoke. + /// + [DllImport("kernel32.dll", EntryPoint = "SetEnvironmentVariable", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetEnvironmentVariableNative(string name, string? value); + + /// + /// Sets an environment variable using P/Invoke to workaround the .NET Framework BCL implementation. + /// + /// + /// .NET Framework implementation of SetEnvironmentVariable checks the length of the value and throws an exception if + /// it's greater than or equal to 32,767 characters. This limitation does not exist on modern Windows or .NET. + /// + internal static void SetEnvironmentVariable(string name, string? value) + { + if (!SetEnvironmentVariableNative(name, value)) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + } + + /// + /// Returns key value pairs of environment variables in a new dictionary + /// with a case-insensitive key comparer. + /// + /// + /// Copied from the BCL implementation to eliminate some expensive security asserts on .NET Framework. + /// + internal static Dictionary GetEnvironmentVariables() + { + unsafe + { + char* pEnvironmentBlock = null; + + try + { + pEnvironmentBlock = GetEnvironmentStrings(); + if (pEnvironmentBlock == null) + { + throw new OutOfMemoryException(); + } + + // Search for terminating \0\0 (two unicode \0's). + char* pEnvironmentBlockEnd = pEnvironmentBlock; + while (!(*pEnvironmentBlockEnd == '\0' && *(pEnvironmentBlockEnd + 1) == '\0')) + { + pEnvironmentBlockEnd++; + } + + long stringBlockLength = pEnvironmentBlockEnd - pEnvironmentBlock; + + // Razzle has 150 environment variables + Dictionary table = new(capacity: 200, StringComparer.OrdinalIgnoreCase); + + // Copy strings out, parsing into pairs and inserting into the table. + // The first few environment variable entries start with an '='! + // The current working directory of every drive (except for those drives + // you haven't cd'ed into in your DOS window) are stored in the + // environment block (as =C:=pwd) and the program's exit code is + // as well (=ExitCode=00000000) Skip all that start with =. + // Read docs about Environment Blocks on MSDN's CreateProcess page. + + // Format for GetEnvironmentStrings is: + // (=HiddenVar=value\0 | Variable=value\0)* \0 + // See the description of Environment Blocks in MSDN's + // CreateProcess page (null-terminated array of null-terminated strings). + // Note the =HiddenVar's aren't always at the beginning. + for (int i = 0; i < stringBlockLength; i++) + { + int startKey = i; + + // Skip to key + // On some old OS, the environment block can be corrupted. + // Some lines will not have '=', so we need to check for '\0'. + while (*(pEnvironmentBlock + i) is not '=' and not '\0') + { + i++; + } + + if (*(pEnvironmentBlock + i) == '\0') + { + continue; + } + + // Skip over environment variables starting with '=' + if (i - startKey == 0) + { + while (*(pEnvironmentBlock + i) != 0) + { + i++; + } + + continue; + } + + string key = new(pEnvironmentBlock, startKey, i - startKey); + + i++; + + // skip over '=' + int startValue = i; + + while (*(pEnvironmentBlock + i) != 0) + { + // Read to end of this entry + i++; + } + + string value = new(pEnvironmentBlock, startValue, i - startValue); + + // skip over 0 handled by for loop's i++ + table[key] = value; + } + + return table; + } + finally + { + if (pEnvironmentBlock != null) + { + FreeEnvironmentStrings(pEnvironmentBlock); + } + } + } + } + + /// + /// Updates the environment to match the provided dictionary. + /// + internal static void SetEnvironment(Dictionary? newEnvironment) + { + if (newEnvironment == null) + { + return; + } + + // First, delete all no longer set variables + Dictionary currentEnvironment = GetEnvironmentVariables(); + foreach (KeyValuePair entry in currentEnvironment) + { + if (!newEnvironment.ContainsKey(entry.Key)) + { + SetEnvironmentVariable(entry.Key, null); + } + } + + // Then, make sure the new ones have their new values. + foreach (KeyValuePair entry in newEnvironment) + { + if (!currentEnvironment.TryGetValue(entry.Key, out string? currentValue) || currentValue != entry.Value) + { + SetEnvironmentVariable(entry.Key, entry.Value); + } + } + } + + /// + /// Indicate to the client that all elements of the Handshake have been sent. + /// + internal static void WriteEndOfHandshakeSignal(this PipeStream stream) + => stream.WriteIntForHandshake(EndOfHandshakeSignal); + + /// + /// Extension method to write a series of bytes to a stream. + /// + internal static void WriteIntForHandshake(this PipeStream stream, int value) + { + byte[] bytes = BitConverter.GetBytes(value); + + // We want to read the long and send it from left to right (this means big endian) + // if we are little endian we need to reverse the array to keep the left to right reading + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + ErrorUtilities.VerifyThrow(bytes.Length == 4, "Int should be 4 bytes"); + + stream.Write(bytes, 0, bytes.Length); + } + + internal static bool TryReadEndOfHandshakeSignal(this PipeStream stream, bool isProvider, out HandshakeResult result) + { + // Accept only the first byte of the EndOfHandshakeSignal + if (stream.TryReadIntForHandshake(byteToAccept: null, out HandshakeResult innerResult)) + { + byte negotiatedPacketVersion = 1; + + if (innerResult.Value != EndOfHandshakeSignal) + { + // If the received handshake part is not PacketVersionFromChildMarker it means we communicate with the host that does not support packet version negotiation. + // Fallback to the old communication validation pattern. + if (innerResult.Value != Handshake.PacketVersionFromChildMarker) + { + result = CreateVersionMismatchResult(isProvider, innerResult.Value); + return false; + } + + // We detected packet version marker, now let's read actual PacketVersion + if (!stream.TryReadIntForHandshake(byteToAccept: null, out HandshakeResult versionResult)) + { + result = versionResult; + return false; + } + + byte childVersion = (byte)versionResult.Value; + negotiatedPacketVersion = NodePacketTypeExtensions.GetNegotiatedPacketVersion(childVersion); + Trace($"Node PacketVersion: {childVersion}, Local: {NodePacketTypeExtensions.PacketVersion}, Negotiated: {negotiatedPacketVersion}"); + + if (!stream.TryReadIntForHandshake(byteToAccept: null, out innerResult)) + { + result = innerResult; + return false; + } + + if (innerResult.Value != EndOfHandshakeSignal) + { + result = CreateVersionMismatchResult(isProvider, innerResult.Value); + return false; + } + } + + result = HandshakeResult.Success(0, negotiatedPacketVersion); + return true; + } + else + { + result = innerResult; + return false; + } + } + + private static HandshakeResult CreateVersionMismatchResult(bool isProvider, int receivedValue) + { + string errorMessage = isProvider + ? $"Handshake failed on part {receivedValue}. Probably the client is a different MSBuild build." + : $"Expected end of handshake signal but received {receivedValue}. Probably the host is a different MSBuild build."; + + Trace(errorMessage); + + return HandshakeResult.Failure(HandshakeStatus.VersionMismatch, errorMessage); + } + + /// + /// Extension method to read a series of bytes from a stream. + /// If specified, leading byte matches one in the supplied array if any, returns rejection byte and throws IOException. + /// + internal static bool TryReadIntForHandshake(this PipeStream stream, byte? byteToAccept, out HandshakeResult result) + { + byte[] bytes = new byte[4]; + int bytesRead = stream.Read(bytes, 0, bytes.Length); + + // Abort for connection attempts from ancient MSBuild.exes + if (byteToAccept != null && bytesRead > 0 && byteToAccept != bytes[0]) + { + stream.WriteIntForHandshake(0x0F0F0F0F); + stream.WriteIntForHandshake(0x0F0F0F0F); + result = HandshakeResult.Failure(HandshakeStatus.OldMSBuild, string.Format(CultureInfo.InvariantCulture, "Client: rejected old host. Received byte {0} instead of {1}.", bytes[0], byteToAccept)); + return false; + } + + if (bytesRead != bytes.Length) + { + // We've unexpectedly reached end of stream. + // We are now in a bad state, disconnect on our end + result = HandshakeResult.Failure(HandshakeStatus.UnexpectedEndOfStream, "Unexpected end of stream while reading for handshake"); + + return false; + } + + try + { + // We want to read the long and send it from left to right (this means big endian) + // If we are little endian the stream has already been reversed by the sender, we need to reverse it again to get the original number + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + result = HandshakeResult.Success(BitConverter.ToInt32(bytes, startIndex: 0)); + } + catch (ArgumentException ex) + { + result = HandshakeResult.Failure(HandshakeStatus.EndiannessMismatch, $"Failed to convert the handshake to big-endian. {ex.Message}"); + return false; + } + + return true; + } + + /// + /// Given the appropriate information, return the equivalent HandshakeOptions. + /// + internal static HandshakeOptions GetHandshakeOptions() + { + // For MSBuildTaskHost, the HandshakeOptions are easy to compute. + HandshakeOptions options = HandshakeOptions.TaskHost; + + options |= HandshakeOptions.CLR2; + + if (NativeMethods.Is64Bit) + { + options |= HandshakeOptions.X64; + } + + // If we are running in elevated privs, we will only accept a handshake from an elevated process as well. + // Both the client and the host will calculate this separately, and the idea is that if they come out the same + // then we can be sufficiently confident that the other side has the same elevation level as us. This is complementary + // to the username check which is also done on connection. + if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) + { + options |= HandshakeOptions.Administrator; + } + + return options; + } + + /// + /// Gets the value of an integer environment variable, or returns the default if none is set or it cannot be converted. + /// + internal static int GetIntegerVariableOrDefault(string environmentVariable, int defaultValue) + { + string environmentValue = Environment.GetEnvironmentVariable(environmentVariable); + + if (string.IsNullOrEmpty(environmentValue) || + !int.TryParse(environmentValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) + { + return defaultValue; + } + + return value; + } + + /// + /// Writes trace information to a log file. + /// + internal static void Trace(string message) + { + if (!s_trace) + { + return; + } + + lock (s_traceLock) + { + s_debugDumpPath ??= Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); + + if (string.IsNullOrEmpty(s_debugDumpPath)) + { + s_debugDumpPath = FileUtilities.TempFileDirectory; + } + else + { + Directory.CreateDirectory(s_debugDumpPath); + } + + try + { + string fileName = $"MSBuild_CommTrace_PID_{EnvironmentUtilities.CurrentProcessId}.txt"; + string filePath = Path.Combine(s_debugDumpPath, fileName); + + using (StreamWriter file = FileUtilities.CreateWriterForAppend(filePath)) + { + long now = DateTime.UtcNow.Ticks; + float millisecondsSinceLastLog = (float)(now - s_lastLoggedTicks) / 10000L; + s_lastLoggedTicks = now; + + file.WriteLine( + "{0} (TID {1}) {2,15} +{3,10}ms: {4}", + Thread.CurrentThread.Name, + Thread.CurrentThread.ManagedThreadId, + now, + millisecondsSinceLastLog, + message); + } + } + catch (IOException) + { + // Ignore + } + } + } + + /// + /// Gets a hash code for this string. If strings A and B are such that A.Equals(B), then + /// they will return the same hash code. + /// This is as implemented in CLR String.GetHashCode() [ndp\clr\src\BCL\system\String.cs] + /// but stripped out architecture specific defines + /// that causes the hashcode to be different and this causes problem in cross-architecture handshaking. + /// + internal static int GetHashCode(string fileVersion) + { + unsafe + { + fixed (char* src = fileVersion) + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + + int* pint = (int*)src; + int len = fileVersion.Length; + while (len > 0) + { + hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; + if (len <= 2) + { + break; + } + + hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1]; + pint += 2; + len -= 4; + } + + return hash1 + (hash2 * 1566083941); + } + } + } + + internal static int AvoidEndOfHandshakeSignal(int x) + => x == EndOfHandshakeSignal ? ~x : x; +} + +/// +/// Represents the components of a handshake in a structured format with named fields. +/// +internal readonly struct HandshakeComponents( + int options, + int salt, + int fileVersionMajor, + int fileVersionMinor, + int fileVersionBuild, + int fileVersionPrivate, + int sessionId) +{ + public int Options => options; + + public int Salt => salt; + + public int FileVersionMajor => fileVersionMajor; + + public int FileVersionMinor => fileVersionMinor; + + public int FileVersionBuild => fileVersionBuild; + + public int FileVersionPrivate => fileVersionPrivate; + + public int SessionId => sessionId; + + public IEnumerable> EnumerateComponents() + { + yield return new KeyValuePair(nameof(Options), Options); + yield return new KeyValuePair(nameof(Salt), Salt); + yield return new KeyValuePair(nameof(FileVersionMajor), FileVersionMajor); + yield return new KeyValuePair(nameof(FileVersionMinor), FileVersionMinor); + yield return new KeyValuePair(nameof(FileVersionBuild), FileVersionBuild); + yield return new KeyValuePair(nameof(FileVersionPrivate), FileVersionPrivate); + yield return new KeyValuePair(nameof(SessionId), SessionId); + } + + public override string ToString() + => $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate} {sessionId}"; +} diff --git a/src/MSBuildTaskHost/Concurrent/ConcurrentDictionary.cs b/src/MSBuildTaskHost/Concurrent/ConcurrentDictionary.cs deleted file mode 100644 index 8cb007b093b..00000000000 --- a/src/MSBuildTaskHost/Concurrent/ConcurrentDictionary.cs +++ /dev/null @@ -1,533 +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 System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; - -#nullable disable - -namespace Microsoft.Build.Shared.Concurrent -{ - // The following class is back-ported from .NET 4.X CoreFX library because - // MSBuildTaskHost requires 3.5 .NET Framework. Only GetOrAdd method kept. - internal class ConcurrentDictionary - { - /// - /// Tables that hold the internal state of the ConcurrentDictionary - /// - /// Wrapping the three tables in a single object allows us to atomically - /// replace all tables at once. - /// - private sealed class Tables - { - internal readonly Node[] _buckets; // A singly-linked list for each bucket. - internal readonly object[] _locks; // A set of locks, each guarding a section of the table. - internal volatile int[] _countPerLock; // The number of elements guarded by each lock. - - internal Tables(Node[] buckets, object[] locks, int[] countPerLock) - { - _buckets = buckets; - _locks = locks; - _countPerLock = countPerLock; - } - } - - private volatile Tables _tables; // Internal tables of the dictionary - private IEqualityComparer _comparer; // Key equality comparer - private readonly bool _growLockArray; // Whether to dynamically increase the size of the striped lock - private int _budget; // The maximum number of elements per lock before a resize operation is triggered - - // The default capacity, i.e. the initial # of buckets. When choosing this value, we are making - // a trade-off between the size of a very small dictionary, and the number of resizes when - // constructing a large dictionary. Also, the capacity should not be divisible by a small prime. - private const int DefaultCapacity = 31; - - // The maximum size of the striped lock that will not be exceeded when locks are automatically - // added as the dictionary grows. However, the user is allowed to exceed this limit by passing - // a concurrency level larger than MaxLockNumber into the constructor. - private const int MaxLockNumber = 1024; - - // Whether TValue is a type that can be written atomically (i.e., with no danger of torn reads) - private static readonly bool s_isValueWriteAtomic = IsValueWriteAtomic(); - - /// - /// Determines whether type TValue can be written atomically - /// - private static bool IsValueWriteAtomic() - { - // - // Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without - // the risk of tearing. - // - // See http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf - // - Type valueType = typeof(TValue); - if (!valueType.IsValueType) - { - return true; - } - - switch (Type.GetTypeCode(valueType)) - { - case TypeCode.Boolean: - case TypeCode.Byte: - case TypeCode.Char: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - return true; - case TypeCode.Int64: - case TypeCode.Double: - case TypeCode.UInt64: - return IntPtr.Size == 8; - default: - return false; - } - } - - /// - /// Initializes a new instance of the - /// class that is empty, has the default concurrency level, has the default initial capacity, and - /// uses the default comparer for the key type. - /// - public ConcurrentDictionary(IEqualityComparer comparer = null) - { - int concurrencyLevel = NativeMethodsShared.GetLogicalCoreCount(); - int capacity = DefaultCapacity; - - // The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard - // any buckets. - if (capacity < concurrencyLevel) - { - capacity = concurrencyLevel; - } - - object[] locks = new object[concurrencyLevel]; - for (int i = 0; i < locks.Length; i++) - { - locks[i] = new object(); - } - - int[] countPerLock = new int[locks.Length]; - Node[] buckets = new Node[capacity]; - _tables = new Tables(buckets, locks, countPerLock); - - _comparer = comparer ?? EqualityComparer.Default; - _growLockArray = true; - _budget = buckets.Length / locks.Length; - } - - private bool TryGetValueInternal(TKey key, int hashcode, out TValue value) - { - Debug.Assert(_comparer.GetHashCode(key) == hashcode); - - // We must capture the _buckets field in a local variable. It is set to a new table on each table resize. - Tables tables = _tables; - - int bucketNo = GetBucket(hashcode, tables._buckets.Length); - - // We can get away w/out a lock here. - // The Volatile.Read ensures that we have a copy of the reference to tables._buckets[bucketNo]. - // This protects us from reading fields ('_hashcode', '_key', '_value' and '_next') of different instances. - Thread.MemoryBarrier(); - Node n = tables._buckets[bucketNo]; - - while (n != null) - { - if (hashcode == n._hashcode && _comparer.Equals(n._key, key)) - { - value = n._value; - return true; - } - n = n._next; - } - - value = default(TValue); - return false; - } - - /// - /// Shared internal implementation for inserts and updates. - /// If key exists, we always return false; and if updateIfExists == true we force update with value; - /// If key doesn't exist, we always add value and return true; - /// - private bool TryAddInternal(TKey key, int hashcode, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue) - { - Debug.Assert(_comparer.GetHashCode(key) == hashcode); - - while (true) - { - int bucketNo, lockNo; - - Tables tables = _tables; - GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); - - bool resizeDesired = false; - bool lockTaken = false; - try - { - if (acquireLock) - { - lockTaken = Monitor.TryEnter(tables._locks[lockNo]); - } - - // If the table just got resized, we may not be holding the right lock, and must retry. - // This should be a rare occurrence. - if (tables != _tables) - { - continue; - } - - // Try to find this key in the bucket - Node prev = null; - for (Node node = tables._buckets[bucketNo]; node != null; node = node._next) - { - Debug.Assert((prev == null && node == tables._buckets[bucketNo]) || prev._next == node); - if (hashcode == node._hashcode && _comparer.Equals(node._key, key)) - { - // The key was found in the dictionary. If updates are allowed, update the value for that key. - // We need to create a new node for the update, in order to support TValue types that cannot - // be written atomically, since lock-free reads may be happening concurrently. - if (updateIfExists) - { - if (s_isValueWriteAtomic) - { - node._value = value; - } - else - { - Node newNode = new Node(node._key, value, hashcode, node._next); - if (prev == null) - { - Interlocked.Exchange(ref tables._buckets[bucketNo], newNode); - } - else - { - prev._next = newNode; - } - } - resultingValue = value; - } - else - { - resultingValue = node._value; - } - return false; - } - prev = node; - } - - // The key was not found in the bucket. Insert the key-value pair. - Interlocked.Exchange(ref tables._buckets[bucketNo], new Node(key, value, hashcode, tables._buckets[bucketNo])); - checked - { - tables._countPerLock[lockNo]++; - } - - // - // If the number of elements guarded by this lock has exceeded the budget, resize the bucket table. - // It is also possible that GrowTable will increase the budget but won't resize the bucket table. - // That happens if the bucket table is found to be poorly utilized due to a bad hash function. - // - if (tables._countPerLock[lockNo] > _budget) - { - resizeDesired = true; - } - } - finally - { - if (lockTaken) - { - Monitor.Exit(tables._locks[lockNo]); - } - } - - // - // The fact that we got here means that we just performed an insertion. If necessary, we will grow the table. - // - // Concurrency notes: - // - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks. - // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0 - // and then verify that the table we passed to it as the argument is still the current table. - // - if (resizeDesired) - { - GrowTable(tables); - } - - resultingValue = value; - return true; - } - } - - private static void ThrowKeyNullException() - { - throw new ArgumentNullException("key"); - } - - /// - /// Adds a key/value pair to the - /// if the key does not already exist. - /// - /// The key of the element to add. - /// The function used to generate a value for the key. - /// is a null reference - /// (Nothing in Visual Basic). - /// is a null reference - /// (Nothing in Visual Basic). - /// The dictionary contains too many - /// elements. - /// The value for the key. This will be either the existing value for the key if the - /// key is already in the dictionary, or the new value for the key as returned by valueFactory - /// if the key was not in the dictionary. - public TValue GetOrAdd(TKey key, Func valueFactory) - { - if (key == null) - { - ThrowKeyNullException(); - } - - if (valueFactory == null) - { - throw new ArgumentNullException(nameof(valueFactory)); - } - - int hashcode = _comparer.GetHashCode(key); - - TValue resultingValue; - if (!TryGetValueInternal(key, hashcode, out resultingValue)) - { - TryAddInternal(key, hashcode, valueFactory(key), false, true, out resultingValue); - } - return resultingValue; - } - - /// - /// Replaces the bucket table with a larger one. To prevent multiple threads from resizing the - /// table as a result of races, the Tables instance that holds the table of buckets deemed too - /// small is passed in as an argument to GrowTable(). GrowTable() obtains a lock, and then checks - /// the Tables instance has been replaced in the meantime or not. - /// - private void GrowTable(Tables tables) - { - const int MaxArrayLength = 0X7FEFFFFF; - int locksAcquired = 0; - try - { - // The thread that first obtains _locks[0] will be the one doing the resize operation - AcquireLocks(0, 1, ref locksAcquired); - - // Make sure nobody resized the table while we were waiting for lock 0: - if (tables != _tables) - { - // We assume that since the table reference is different, it was already resized (or the budget - // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons, - // we will have to revisit this logic. - return; - } - - // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow. - long approxCount = 0; - for (int i = 0; i < tables._countPerLock.Length; i++) - { - approxCount += tables._countPerLock[i]; - } - - // - // If the bucket array is too empty, double the budget instead of resizing the table - // - if (approxCount < tables._buckets.Length / 4) - { - _budget = 2 * _budget; - if (_budget < 0) - { - _budget = int.MaxValue; - } - return; - } - - - // Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by - // 2,3,5 or 7. We can consider a different table-sizing policy in the future. - int newLength = 0; - bool maximizeTableSize = false; - try - { - checked - { - // Double the size of the buckets table and add one, so that we have an odd integer. - newLength = (tables._buckets.Length * 2) + 1; - - // Now, we only need to check odd integers, and find the first that is not divisible - // by 3, 5 or 7. - while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) - { - newLength += 2; - } - - Debug.Assert(newLength % 2 != 0); - - if (newLength > MaxArrayLength) - { - maximizeTableSize = true; - } - } - } - catch (OverflowException) - { - maximizeTableSize = true; - } - - if (maximizeTableSize) - { - newLength = MaxArrayLength; - - // We want to make sure that GrowTable will not be called again, since table is at the maximum size. - // To achieve that, we set the budget to int.MaxValue. - // - // (There is one special case that would allow GrowTable() to be called in the future: - // calling Clear() on the ConcurrentDictionary will shrink the table and lower the budget.) - _budget = int.MaxValue; - } - - // Now acquire all other locks for the table - AcquireLocks(1, tables._locks.Length, ref locksAcquired); - - object[] newLocks = tables._locks; - - // Add more locks - if (_growLockArray && tables._locks.Length < MaxLockNumber) - { - newLocks = new object[tables._locks.Length * 2]; - Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); - for (int i = tables._locks.Length; i < newLocks.Length; i++) - { - newLocks[i] = new object(); - } - } - - Node[] newBuckets = new Node[newLength]; - int[] newCountPerLock = new int[newLocks.Length]; - - // Copy all data into a new table, creating new nodes for all elements - for (int i = 0; i < tables._buckets.Length; i++) - { - Node current = tables._buckets[i]; - while (current != null) - { - Node next = current._next; - int newBucketNo, newLockNo; - GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length); - - newBuckets[newBucketNo] = new Node(current._key, current._value, current._hashcode, newBuckets[newBucketNo]); - - checked - { - newCountPerLock[newLockNo]++; - } - - current = next; - } - } - - // Adjust the budget - _budget = Math.Max(1, newBuckets.Length / newLocks.Length); - - // Replace tables with the new versions - _tables = new Tables(newBuckets, newLocks, newCountPerLock); - } - finally - { - // Release all locks that we took earlier - ReleaseLocks(0, locksAcquired); - } - } - - /// - /// Computes the bucket for a particular key. - /// - private static int GetBucket(int hashcode, int bucketCount) - { - int bucketNo = (hashcode & 0x7fffffff) % bucketCount; - Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); - return bucketNo; - } - - /// - /// Computes the bucket and lock number for a particular key. - /// - private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) - { - bucketNo = (hashcode & 0x7fffffff) % bucketCount; - lockNo = bucketNo % lockCount; - - Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); - Debug.Assert(lockNo >= 0 && lockNo < lockCount); - } - - /// - /// Acquires a contiguous range of locks for this hash table, and increments locksAcquired - /// by the number of locks that were successfully acquired. The locks are acquired in an - /// increasing order. - /// - private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) - { - Debug.Assert(fromInclusive <= toExclusive); - object[] locks = _tables._locks; - - for (int i = fromInclusive; i < toExclusive; i++) - { - bool lockTaken = false; - try - { - lockTaken = Monitor.TryEnter(locks[i]); - } - finally - { - if (lockTaken) - { - locksAcquired++; - } - } - } - } - - /// - /// Releases a contiguous range of locks. - /// - private void ReleaseLocks(int fromInclusive, int toExclusive) - { - Debug.Assert(fromInclusive <= toExclusive); - - for (int i = fromInclusive; i < toExclusive; i++) - { - Monitor.Exit(_tables._locks[i]); - } - } - - /// - /// A node in a singly-linked list representing a particular hash table bucket. - /// - private sealed class Node - { - internal readonly TKey _key; - internal TValue _value; - internal volatile Node _next; - internal readonly int _hashcode; - - internal Node(TKey key, TValue value, int hashcode, Node next) - { - _key = key; - _value = value; - _next = next; - _hashcode = hashcode; - } - } - } -} diff --git a/src/MSBuildTaskHost/Concurrent/ConcurrentQueue.cs b/src/MSBuildTaskHost/Concurrent/ConcurrentQueue.cs deleted file mode 100644 index b1b05998bda..00000000000 --- a/src/MSBuildTaskHost/Concurrent/ConcurrentQueue.cs +++ /dev/null @@ -1,567 +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 System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; - -#nullable disable - -namespace Microsoft.Build.Shared.Concurrent -{ - // The following class is back-ported from .NET 4.X CoreFX library because - // MSBuildTaskHost requires 3.5 .NET Framework. Only important methods (Enqueue, TryDequeue) are kept. - internal class ConcurrentQueue - { - // This implementation provides an unbounded, multi-producer multi-consumer queue - // that supports the standard Enqueue/TryDequeue operations, as well as support for - // snapshot enumeration (GetEnumerator, ToArray, CopyTo), peeking, and Count/IsEmpty. - // It is composed of a linked list of bounded ring buffers, each of which has a head - // and a tail index, isolated from each other to minimize false sharing. As long as - // the number of elements in the queue remains less than the size of the current - // buffer (Segment), no additional allocations are required for enqueued items. When - // the number of items exceeds the size of the current segment, the current segment is - // "frozen" to prevent further enqueues, and a new segment is linked from it and set - // as the new tail segment for subsequent enqueues. As old segments are consumed by - // dequeues, the head reference is updated to point to the segment that dequeuers should - // try next. To support snapshot enumeration, segments also support the notion of - // preserving for observation, whereby they avoid overwriting state as part of dequeues. - // Any operation that requires a snapshot results in all current segments being - // both frozen for enqueues and preserved for observation: any new enqueues will go - // to new segments, and dequeuers will consume from the existing segments but without - // overwriting the existing data. - - /// Initial length of the segments used in the queue. - private const int InitialSegmentLength = 32; - /// - /// Maximum length of the segments used in the queue. This is a somewhat arbitrary limit: - /// larger means that as long as we don't exceed the size, we avoid allocating more segments, - /// but if we do exceed it, then the segment becomes garbage. - /// - private const int MaxSegmentLength = 1024 * 1024; - - /// - /// Lock used to protect cross-segment operations, including any updates to or - /// and any operations that need to get a consistent view of them. - /// - private object _crossSegmentLock; - /// The current tail segment. - private volatile Segment _tail; - /// The current head segment. - private volatile Segment _head; - - internal static object VolatileReader(ref object o) => Thread.VolatileRead(ref o); - /// - /// Initializes a new instance of the class. - /// - public ConcurrentQueue() - { - _crossSegmentLock = new object(); - _tail = _head = new Segment(InitialSegmentLength); - } - - /// Adds an object to the end of the . - /// - /// The object to add to the end of the . - /// The value can be a null reference (Nothing in Visual Basic) for reference types. - /// - public void Enqueue(T item) - { - // Try to enqueue to the current tail. - if (!_tail.TryEnqueue(item)) - { - // If we're unable to, we need to take a slow path that will - // try to add a new tail segment. - EnqueueSlow(item); - } - } - - /// Adds to the end of the queue, adding a new segment if necessary. - private void EnqueueSlow(T item) - { - while (true) - { - Segment tail = _tail; - - // Try to append to the existing tail. - if (tail.TryEnqueue(item)) - { - return; - } - - // If we were unsuccessful, take the lock so that we can compare and manipulate - // the tail. Assuming another enqueuer hasn't already added a new segment, - // do so, then loop around to try enqueueing again. - lock (_crossSegmentLock) - { - if (tail == _tail) - { - // Make sure no one else can enqueue to this segment. - tail.EnsureFrozenForEnqueues(); - - // We determine the new segment's length based on the old length. - // In general, we double the size of the segment, to make it less likely - // that we'll need to grow again. However, if the tail segment is marked - // as preserved for observation, something caused us to avoid reusing this - // segment, and if that happens a lot and we grow, we'll end up allocating - // lots of wasted space. As such, in such situations we reset back to the - // initial segment length; if these observations are happening frequently, - // this will help to avoid wasted memory, and if they're not, we'll - // relatively quickly grow again to a larger size. - int nextSize = tail._preservedForObservation != 0 ? InitialSegmentLength : Math.Min(tail.Capacity * 2, MaxSegmentLength); - var newTail = new Segment(nextSize); - - // Hook up the new tail. - tail._nextSegment = newTail; - _tail = newTail; - } - } - } - } - - /// - /// Attempts to remove and return the object at the beginning of the . - /// - /// - /// When this method returns, if the operation was successful, contains the - /// object removed. If no object was available to be removed, the value is unspecified. - /// - /// - /// true if an element was removed and returned from the beginning of the - /// successfully; otherwise, false. - /// - public bool TryDequeue(out T result) => - _head.TryDequeue(out result) || // fast-path that operates just on the head segment - TryDequeueSlow(out result); // slow path that needs to fix up segments - - /// Tries to dequeue an item, removing empty segments as needed. - private bool TryDequeueSlow(out T item) - { - while (true) - { - // Get the current head - Segment head = _head; - - // Try to take. If we're successful, we're done. - if (head.TryDequeue(out item)) - { - return true; - } - - // Check to see whether this segment is the last. If it is, we can consider - // this to be a moment-in-time empty condition (even though between the TryDequeue - // check and this check, another item could have arrived). - if (head._nextSegment == null) - { - item = default(T); - return false; - } - - // At this point we know that head.Next != null, which means - // this segment has been frozen for additional enqueues. But between - // the time that we ran TryDequeue and checked for a next segment, - // another item could have been added. Try to dequeue one more time - // to confirm that the segment is indeed empty. - Debug.Assert(head._frozenForEnqueues); - if (head.TryDequeue(out item)) - { - return true; - } - - // This segment is frozen (nothing more can be added) and empty (nothing is in it). - // Update head to point to the next segment in the list, assuming no one's beat us to it. - lock (_crossSegmentLock) - { - if (head == _head) - { - _head = head._nextSegment; - } - } - } - } - - /// - /// Attempts to return an object from the beginning of the - /// without removing it. - /// - /// - /// When this method returns, contains an object from - /// the beginning of the or default(T) - /// if the operation failed. - /// - /// true if and object was returned successfully; otherwise, false. - /// - /// For determining whether the collection contains any items, use of the - /// property is recommended rather than peeking. - /// - public bool TryPeek(out T result) => TryPeek(out result, resultUsed: true); - - /// Attempts to retrieve the value for the first element in the queue. - /// The value of the first element, if found. - /// true if the result is neede; otherwise false if only the true/false outcome is needed. - /// true if an element was found; otherwise, false. - private bool TryPeek(out T result, bool resultUsed) - { - // Starting with the head segment, look through all of the segments - // for the first one we can find that's not empty. - Segment s = _head; - while (true) - { - // Grab the next segment from this one, before we peek. - // This is to be able to see whether the value has changed - // during the peek operation. - Thread.MemoryBarrier(); - Segment next = s._nextSegment; - - // Peek at the segment. If we find an element, we're done. - if (s.TryPeek(out result, resultUsed)) - { - return true; - } - - // The current segment was empty at the moment we checked. - - if (next != null) - { - // If prior to the peek there was already a next segment, then - // during the peek no additional items could have been enqueued - // to it and we can just move on to check the next segment. - Debug.Assert(next == s._nextSegment); - s = next; - } - else - { - Thread.MemoryBarrier(); - if (s._nextSegment == null) - { - // The next segment is null. Nothing more to peek at. - break; - } - } - - // The next segment was null before we peeked but non-null after. - // That means either when we peeked the first segment had - // already been frozen but the new segment not yet added, - // or that the first segment was empty and between the time - // that we peeked and then checked _nextSegment, so many items - // were enqueued that we filled the first segment and went - // into the next. Since we need to peek in order, we simply - // loop around again to peek on the same segment. The next - // time around on this segment we'll then either successfully - // peek or we'll find that next was non-null before peeking, - // and we'll traverse to that segment. - } - - result = default(T); - return false; - } - - /// - /// Provides a multi-producer, multi-consumer thread-safe bounded segment. When the queue is full, - /// enqueues fail and return false. When the queue is empty, dequeues fail and return null. - /// These segments are linked together to form the unbounded . - /// - [DebuggerDisplay("Capacity = {Capacity}")] - private sealed class Segment - { - // Segment design is inspired by the algorithm outlined at: - // http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue - - /// The array of items in this queue. Each slot contains the item in that slot and its "sequence number". - internal readonly Slot[] _slots; - /// Mask for quickly accessing a position within the queue's array. - internal readonly int _slotsMask; - /// The head and tail positions, with padding to help avoid false sharing contention. - /// Dequeuing happens from the head, enqueuing happens at the tail. - internal PaddedHeadAndTail _headAndTail; // mutable struct: do not make this readonly - - /// Indicates whether the segment has been marked such that dequeues don't overwrite the removed data. - internal byte _preservedForObservation; - /// Indicates whether the segment has been marked such that no additional items may be enqueued. - internal bool _frozenForEnqueues; - /// The segment following this one in the queue, or null if this segment is the last in the queue. - internal Segment _nextSegment; - - /// Creates the segment. - /// - /// The maximum number of elements the segment can contain. Must be a power of 2. - /// - public Segment(int boundedLength) - { - // Validate the length - Debug.Assert(boundedLength >= 2, $"Must be >= 2, got {boundedLength}"); - Debug.Assert((boundedLength & (boundedLength - 1)) == 0, $"Must be a power of 2, got {boundedLength}"); - - // Initialize the slots and the mask. The mask is used as a way of quickly doing "% _slots.Length", - // instead letting us do "& _slotsMask". - _slots = new Slot[boundedLength]; - _slotsMask = boundedLength - 1; - - // Initialize the sequence number for each slot. The sequence number provides a ticket that - // allows dequeuers to know whether they can dequeue and enqueuers to know whether they can - // enqueue. An enqueuer at position N can enqueue when the sequence number is N, and a dequeuer - // for position N can dequeue when the sequence number is N + 1. When an enqueuer is done writing - // at position N, it sets the sequence number to N + 1 so that a dequeuer will be able to dequeue, - // and when a dequeuer is done dequeueing at position N, it sets the sequence number to N + _slots.Length, - // so that when an enqueuer loops around the slots, it'll find that the sequence number at - // position N is N. This also means that when an enqueuer finds that at position N the sequence - // number is < N, there is still a value in that slot, i.e. the segment is full, and when a - // dequeuer finds that the value in a slot is < N + 1, there is nothing currently available to - // dequeue. (It is possible for multiple enqueuers to enqueue concurrently, writing into - // subsequent slots, and to have the first enqueuer take longer, so that the slots for 1, 2, 3, etc. - // may have values, but the 0th slot may still be being filled... in that case, TryDequeue will - // return false.) - for (int i = 0; i < _slots.Length; i++) - { - _slots[i].SequenceNumber = i; - } - } - - /// Gets the number of elements this segment can store. - internal int Capacity => _slots.Length; - - /// Gets the "freeze offset" for this segment. - internal int FreezeOffset => _slots.Length * 2; - - /// - /// Ensures that the segment will not accept any subsequent enqueues that aren't already underway. - /// - /// - /// When we mark a segment as being frozen for additional enqueues, - /// we set the bool, but that's mostly - /// as a small helper to avoid marking it twice. The real marking comes - /// by modifying the Tail for the segment, increasing it by this - /// . This effectively knocks it off the - /// sequence expected by future enqueuers, such that any additional enqueuer - /// will be unable to enqueue due to it not lining up with the expected - /// sequence numbers. This value is chosen specially so that Tail will grow - /// to a value that maps to the same slot but that won't be confused with - /// any other enqueue/dequeue sequence number. - /// - internal void EnsureFrozenForEnqueues() // must only be called while queue's segment lock is held - { - if (!_frozenForEnqueues) // flag used to ensure we don't increase the Tail more than once if frozen more than once - { - _frozenForEnqueues = true; - - // Increase the tail by FreezeOffset, spinning until we're successful in doing so. - while (true) - { - int tail = Thread.VolatileRead(ref _headAndTail.Tail); - if (Interlocked.CompareExchange(ref _headAndTail.Tail, tail + FreezeOffset, tail) == tail) - { - break; - } - Thread.SpinWait(1); - } - } - } - - /// Tries to dequeue an element from the queue. - public bool TryDequeue(out T item) - { - // Loop in case of contention... - while (true) - { - // Get the head at which to try to dequeue. - int currentHead = Thread.VolatileRead(ref _headAndTail.Head); - int slotsIndex = currentHead & _slotsMask; - - // Read the sequence number for the head position. - int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); - - // We can dequeue from this slot if it's been filled by an enqueuer, which - // would have left the sequence number at pos+1. - int diff = sequenceNumber - (currentHead + 1); - if (diff == 0) - { - // We may be racing with other dequeuers. Try to reserve the slot by incrementing - // the head. Once we've done that, no one else will be able to read from this slot, - // and no enqueuer will be able to read from this slot until we've written the new - // sequence number. WARNING: The next few lines are not reliable on a runtime that - // supports thread aborts. If a thread abort were to sneak in after the CompareExchange - // but before the Volatile.Write, enqueuers trying to enqueue into this slot would - // spin indefinitely. If this implementation is ever used on such a platform, this - // if block should be wrapped in a finally / prepared region. - if (Interlocked.CompareExchange(ref _headAndTail.Head, currentHead + 1, currentHead) == currentHead) - { - // Successfully reserved the slot. Note that after the above CompareExchange, other threads - // trying to dequeue from this slot will end up spinning until we do the subsequent Write. - item = _slots[slotsIndex].Item; - if (Thread.VolatileRead(ref _preservedForObservation) == 0) - { - // If we're preserving, though, we don't zero out the slot, as we need it for - // enumerations, peeking, ToArray, etc. And we don't update the sequence number, - // so that an enqueuer will see it as full and be forced to move to a new segment. - _slots[slotsIndex].Item = default(T); - Thread.VolatileWrite(ref _slots[slotsIndex].SequenceNumber, currentHead + _slots.Length); - } - return true; - } - } - else if (diff < 0) - { - // The sequence number was less than what we needed, which means this slot doesn't - // yet contain a value we can dequeue, i.e. the segment is empty. Technically it's - // possible that multiple enqueuers could have written concurrently, with those - // getting later slots actually finishing first, so there could be elements after - // this one that are available, but we need to dequeue in order. So before declaring - // failure and that the segment is empty, we check the tail to see if we're actually - // empty or if we're just waiting for items in flight or after this one to become available. - bool frozen = _frozenForEnqueues; - int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); - if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0))) - { - item = default(T); - return false; - } - - // It's possible it could have become frozen after we checked _frozenForEnqueues - // and before reading the tail. That's ok: in that rare race condition, we just - // loop around again. - } - - // Lost a race. Spin a bit, then try again. - Thread.SpinWait(1); - } - } - - /// Tries to peek at an element from the queue, without removing it. - public bool TryPeek(out T result, bool resultUsed) - { - if (resultUsed) - { - // In order to ensure we don't get a torn read on the value, we mark the segment - // as preserving for observation. Additional items can still be enqueued to this - // segment, but no space will be freed during dequeues, such that the segment will - // no longer be reusable. - _preservedForObservation = 1; - Thread.MemoryBarrier(); - } - - // Loop in case of contention... - while (true) - { - // Get the head at which to try to peek. - int currentHead = Thread.VolatileRead(ref _headAndTail.Head); - int slotsIndex = currentHead & _slotsMask; - - // Read the sequence number for the head position. - int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); - - // We can peek from this slot if it's been filled by an enqueuer, which - // would have left the sequence number at pos+1. - int diff = sequenceNumber - (currentHead + 1); - if (diff == 0) - { - result = resultUsed ? _slots[slotsIndex].Item : default(T); - return true; - } - else if (diff < 0) - { - // The sequence number was less than what we needed, which means this slot doesn't - // yet contain a value we can peek, i.e. the segment is empty. Technically it's - // possible that multiple enqueuers could have written concurrently, with those - // getting later slots actually finishing first, so there could be elements after - // this one that are available, but we need to peek in order. So before declaring - // failure and that the segment is empty, we check the tail to see if we're actually - // empty or if we're just waiting for items in flight or after this one to become available. - bool frozen = _frozenForEnqueues; - int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); - if (currentTail - currentHead <= 0 || (frozen && (currentTail - FreezeOffset - currentHead <= 0))) - { - result = default(T); - return false; - } - - // It's possible it could have become frozen after we checked _frozenForEnqueues - // and before reading the tail. That's ok: in that rare race condition, we just - // loop around again. - } - - // Lost a race. Spin a bit, then try again. - Thread.SpinWait(1); - } - } - - /// - /// Attempts to enqueue the item. If successful, the item will be stored - /// in the queue and true will be returned; otherwise, the item won't be stored, and false - /// will be returned. - /// - public bool TryEnqueue(T item) - { - // Loop in case of contention... - while (true) - { - // Get the tail at which to try to return. - int currentTail = Thread.VolatileRead(ref _headAndTail.Tail); - int slotsIndex = currentTail & _slotsMask; - - // Read the sequence number for the tail position. - int sequenceNumber = Thread.VolatileRead(ref _slots[slotsIndex].SequenceNumber); - - // The slot is empty and ready for us to enqueue into it if its sequence - // number matches the slot. - int diff = sequenceNumber - currentTail; - if (diff == 0) - { - // We may be racing with other enqueuers. Try to reserve the slot by incrementing - // the tail. Once we've done that, no one else will be able to write to this slot, - // and no dequeuer will be able to read from this slot until we've written the new - // sequence number. WARNING: The next few lines are not reliable on a runtime that - // supports thread aborts. If a thread abort were to sneak in after the CompareExchange - // but before the Volatile.Write, other threads will spin trying to access this slot. - // If this implementation is ever used on such a platform, this if block should be - // wrapped in a finally / prepared region. - if (Interlocked.CompareExchange(ref _headAndTail.Tail, currentTail + 1, currentTail) == currentTail) - { - // Successfully reserved the slot. Note that after the above CompareExchange, other threads - // trying to return will end up spinning until we do the subsequent Write. - _slots[slotsIndex].Item = item; - Thread.VolatileWrite(ref _slots[slotsIndex].SequenceNumber, currentTail + 1); - return true; - } - } - else if (diff < 0) - { - // The sequence number was less than what we needed, which means this slot still - // contains a value, i.e. the segment is full. Technically it's possible that multiple - // dequeuers could have read concurrently, with those getting later slots actually - // finishing first, so there could be spaces after this one that are available, but - // we need to enqueue in order. - return false; - } - - // Lost a race. Spin a bit, then try again. - Thread.SpinWait(1); - } - } - - /// Represents a slot in the queue. - [StructLayout(LayoutKind.Auto)] - [DebuggerDisplay("Item = {Item}, SequenceNumber = {SequenceNumber}")] - internal struct Slot - { - /// The item. - public T Item; - /// The sequence number for this slot, used to synchronize between enqueuers and dequeuers. - public int SequenceNumber; - } - } - } - - /// Padded head and tail indices, to avoid false sharing between producers and consumers. - [DebuggerDisplay("Head = {Head}, Tail = {Tail}")] - [StructLayout(LayoutKind.Explicit, Size = 192)] // padding before/between/after fields based on typical cache line size of 64 - internal struct PaddedHeadAndTail - { - [FieldOffset(64)] - public int Head; - - [FieldOffset(128)] - public int Tail; - } -} diff --git a/src/MSBuildTaskHost/Exceptions/BuildExceptionBase.cs b/src/MSBuildTaskHost/Exceptions/BuildExceptionBase.cs new file mode 100644 index 00000000000..5f4cb280367 --- /dev/null +++ b/src/MSBuildTaskHost/Exceptions/BuildExceptionBase.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using Microsoft.Build.TaskHost.BackEnd; + +namespace Microsoft.Build.TaskHost.Exceptions; + +internal abstract class BuildExceptionBase : Exception +{ + private string? _remoteTypeName; + private string? _remoteStackTrace; + + private protected BuildExceptionBase() + : base() + { + } + + private protected BuildExceptionBase(string? message) + : base(message) + { + } + + private protected BuildExceptionBase(string? message, Exception? inner) + : base(message, inner) + { + } + + // This is needed to allow opting back in to BinaryFormatter serialization + private protected BuildExceptionBase(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + public override string? StackTrace + => string.IsNullOrEmpty(_remoteStackTrace) ? base.StackTrace : _remoteStackTrace; + + public override string ToString() + => string.IsNullOrEmpty(_remoteTypeName) ? base.ToString() : $"{_remoteTypeName}->{base.ToString()}"; + + /// + /// Override this method to recover subtype-specific state from the remote exception. + /// + protected virtual void InitializeCustomState(IDictionary? customKeyedSerializedData) + { + } + + /// + /// Override this method to provide subtype-specific state to be serialized. + /// + /// + protected virtual IDictionary? FlushCustomState() => null; + + private void InitializeFromRemoteState(BuildExceptionRemoteState remoteState) + { + _remoteTypeName = remoteState.RemoteTypeName; + _remoteStackTrace = remoteState.RemoteStackTrace; + + Source = remoteState.Source; + HelpLink = remoteState.HelpLink; + HResult = remoteState.HResult; + + if (remoteState.Source != null) + { + InitializeCustomState(remoteState.CustomKeyedSerializedData); + } + } + + public static void WriteExceptionToTranslator(ITranslator translator, Exception exception) + { + BinaryWriter writer = translator.Writer; + writer.Write(exception.InnerException != null); + if (exception.InnerException != null) + { + WriteExceptionToTranslator(translator, exception.InnerException); + } + + string serializationType = BuildExceptionSerializationHelper.GetSerializationKey(exception.GetType()); + writer.Write(serializationType); + writer.Write(exception.Message); + writer.WriteOptionalString(exception.StackTrace); + writer.WriteOptionalString(exception.Source); + writer.WriteOptionalString(exception.HelpLink); + + // HResult is completely protected up till net4.5 + int? hresult = null; + writer.WriteOptionalInt32(hresult); + + IDictionary? customKeyedSerializedData = (exception as BuildExceptionBase)?.FlushCustomState(); + if (customKeyedSerializedData == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(customKeyedSerializedData.Count); + foreach (var pair in customKeyedSerializedData) + { + writer.Write(pair.Key); + writer.WriteOptionalString(pair.Value); + } + } + + Debug.Assert((exception.Data?.Count ?? 0) == 0, + "Exception Data is not supported in BuildTransferredException"); + } + + public static Exception ReadExceptionFromTranslator(ITranslator translator) + { + BinaryReader reader = translator.Reader; + Exception? innerException = null; + if (reader.ReadBoolean()) + { + innerException = ReadExceptionFromTranslator(translator); + } + + string serializationType = reader.ReadString(); + string message = reader.ReadString(); + string? deserializedStackTrace = reader.ReadOptionalString(); + string? source = reader.ReadOptionalString(); + string? helpLink = reader.ReadOptionalString(); + int hResult = reader.ReadOptionalInt32() ?? 0; + + IDictionary? customKeyedSerializedData = null; + if (reader.ReadByte() == 1) + { + int count = reader.ReadInt32(); + customKeyedSerializedData = new Dictionary(count, StringComparer.CurrentCulture); + + for (int i = 0; i < count; i++) + { + customKeyedSerializedData[reader.ReadString()] = reader.ReadOptionalString(); + } + } + + BuildExceptionBase exception = BuildExceptionSerializationHelper.DeserializeException(serializationType, message, innerException); + + exception.InitializeFromRemoteState( + new BuildExceptionRemoteState( + serializationType, + deserializedStackTrace, + source, + helpLink, + hResult, + customKeyedSerializedData)); + + return exception; + } +} diff --git a/src/MSBuildTaskHost/Exceptions/BuildExceptionRemoteState.cs b/src/MSBuildTaskHost/Exceptions/BuildExceptionRemoteState.cs new file mode 100644 index 00000000000..1281506277c --- /dev/null +++ b/src/MSBuildTaskHost/Exceptions/BuildExceptionRemoteState.cs @@ -0,0 +1,40 @@ +// 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; + +namespace Microsoft.Build.TaskHost.Exceptions; + +/// +/// Remote exception internal data serving as the source for the exception deserialization. +/// +internal class BuildExceptionRemoteState +{ + public string RemoteTypeName { get; } + + public string? RemoteStackTrace { get; } + + public string? Source { get; } + + public string? HelpLink { get; } + + public int HResult { get; } + + public IDictionary? CustomKeyedSerializedData { get; } + + public BuildExceptionRemoteState( + string remoteTypeName, + string? remoteStackTrace, + string? source, + string? helpLink, + int hResult, + IDictionary? customKeyedSerializedData) + { + RemoteTypeName = remoteTypeName; + RemoteStackTrace = remoteStackTrace; + Source = source; + HelpLink = helpLink; + HResult = hResult; + CustomKeyedSerializedData = customKeyedSerializedData; + } +} diff --git a/src/MSBuildTaskHost/Exceptions/BuildExceptionSerializationHelper.cs b/src/MSBuildTaskHost/Exceptions/BuildExceptionSerializationHelper.cs new file mode 100644 index 00000000000..c2be08cbf6e --- /dev/null +++ b/src/MSBuildTaskHost/Exceptions/BuildExceptionSerializationHelper.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.TaskHost.Exceptions; + +internal static class BuildExceptionSerializationHelper +{ + private static readonly Dictionary> s_exceptionFactories = new() + { + { GetSerializationKey(), InternalErrorException.CreateFromRemote } + }; + + private static readonly Func s_defaultFactory = + (message, innerException) => new GeneralBuildTransferredException(message, innerException); + + public static string GetSerializationKey() + where T : BuildExceptionBase + => GetSerializationKey(typeof(T)); + + public static string GetSerializationKey(Type exceptionType) + => exceptionType.FullName ?? exceptionType.ToString(); + + public static BuildExceptionBase DeserializeException(string serializationType, string message, Exception? innerException) + { + if (!s_exceptionFactories.TryGetValue(serializationType, out var factory)) + { + factory = s_defaultFactory; + } + + return factory(message, innerException); + } +} diff --git a/src/MSBuildTaskHost/Exceptions/GeneralBuildTransferredException.cs b/src/MSBuildTaskHost/Exceptions/GeneralBuildTransferredException.cs new file mode 100644 index 00000000000..9a524d89cd7 --- /dev/null +++ b/src/MSBuildTaskHost/Exceptions/GeneralBuildTransferredException.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.TaskHost.Exceptions; + +/// +/// A catch-all type for remote exceptions that we don't know how to deserialize. +/// +internal sealed class GeneralBuildTransferredException : BuildExceptionBase +{ + public GeneralBuildTransferredException() + : base() + { + } + + internal GeneralBuildTransferredException(string message, Exception? innerException) + : base(message, innerException) + { + } +} diff --git a/src/MSBuildTaskHost/Exceptions/InternalErrorException.cs b/src/MSBuildTaskHost/Exceptions/InternalErrorException.cs new file mode 100644 index 00000000000..688958af533 --- /dev/null +++ b/src/MSBuildTaskHost/Exceptions/InternalErrorException.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Microsoft.Build.TaskHost.Exceptions; + +/// +/// This exception is to be thrown whenever an assumption we have made in the code turns out to be false. Thus, if this +/// exception ever gets thrown, it is because of a bug in our own code, not because of something the user or project author +/// did wrong. +/// +[Serializable] +internal sealed class InternalErrorException : BuildExceptionBase +{ + private InternalErrorException() + : base() + { + } + + internal InternalErrorException(string message) + : base($"MSB0001: Internal MSBuild Error: {message}") + { + ConsiderDebuggerLaunch(message, null); + } + + /// + /// Creates an instance of this exception using the given message and inner exception. + /// Adds the inner exception's details to the exception message because most bug reporters don't bother + /// to provide the inner exception details which is typically what we care about. + /// + internal InternalErrorException(string message, Exception? innerException) + : this(message, innerException, calledFromDeserialization: false) + { + } + + internal static InternalErrorException CreateFromRemote(string message, Exception? innerException) + => new(message, innerException, calledFromDeserialization: true); + + private InternalErrorException(string message, Exception? innerException, bool calledFromDeserialization) + : base(GetMessage(calledFromDeserialization, message, innerException)) + { + if (!calledFromDeserialization) + { + ConsiderDebuggerLaunch(message, innerException); + } + } + + private static string GetMessage(bool calledFromDeserialization, string message, Exception? innerException) + => calledFromDeserialization + ? message + : innerException is null + ? $"MSB0001: Internal MSBuild Error: {message}" + : $"MSB0001: Internal MSBuild Error: {message}\n=============\n{innerException}\n\n"; + + /// + /// Private constructor used for (de)serialization. The constructor is private as this class is sealed + /// If we ever add new members to this class, we'll need to update this. + /// + private InternalErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + // Do nothing: no fields + } + + /// + /// A fatal internal error due to a bug has occurred. Give the dev a chance to debug it, if possible. + /// + /// Will in all cases launch the debugger, if the environment variable "MSBUILDLAUNCHDEBUGGER" is set. + /// + /// In DEBUG build, will always launch the debugger, unless we are in razzle (_NTROOT is set) or in NUnit, + /// or MSBUILDDONOTLAUNCHDEBUGGER is set (that could be useful in suite runs). + /// We don't launch in retail or LKG so builds don't jam; they get a callstack, and continue or send a mail, etc. + /// We don't launch in NUnit as tests often intentionally cause InternalErrorExceptions. + /// + /// Because we only call this method from this class, just before throwing an InternalErrorException, there is + /// no danger that this suppression will cause a bug to only manifest itself outside NUnit + /// (which would be most unfortunate!). Do not make this non-private. + /// + /// Unfortunately NUnit can't handle unhandled exceptions like InternalErrorException on anything other than + /// the main test thread. However, there's still a callstack displayed before it quits. + /// + /// If it is going to launch the debugger, it first does a Debug.Fail to give information about what needs to + /// be debugged -- the exception hasn't been thrown yet. This automatically displays the current callstack. + /// + private static void ConsiderDebuggerLaunch(string message, Exception? innerException) + { + string innerMessage = innerException == null ? string.Empty : innerException.ToString(); + + if (Environment.GetEnvironmentVariable("MSBUILDLAUNCHDEBUGGER") != null) + { + LaunchDebugger(message, innerMessage); + return; + } + +#if DEBUG + if (Environment.GetEnvironmentVariable("MSBUILDDONOTLAUNCHDEBUGGER") == null + && Environment.GetEnvironmentVariable("_NTROOT") == null) + { + LaunchDebugger(message, innerMessage); + return; + } +#endif + } + + private static void LaunchDebugger(string message, string innerMessage) + { +#if FEATURE_DEBUG_LAUNCH + Debug.Fail(message, innerMessage); + Debugger.Launch(); +#else + Console.WriteLine("MSBuild Failure: " + message); + if (!string.IsNullOrEmpty(innerMessage)) + { + Console.WriteLine(innerMessage); + } + Console.WriteLine("Waiting for debugger to attach to process: " + Process.GetCurrentProcess().Id); + while (!Debugger.IsAttached) + { + System.Threading.Thread.Sleep(100); + } +#endif + } +} diff --git a/src/MSBuildTaskHost/FileSystem/MSBuildTaskHostFileSystem.cs b/src/MSBuildTaskHost/FileSystem/MSBuildTaskHostFileSystem.cs deleted file mode 100644 index 46cb997dc51..00000000000 --- a/src/MSBuildTaskHost/FileSystem/MSBuildTaskHostFileSystem.cs +++ /dev/null @@ -1,83 +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 System; -using System.Collections.Generic; -using System.IO; - -#nullable disable - -namespace Microsoft.Build.Shared.FileSystem -{ - /// - /// Legacy implementation for MSBuildTaskHost which is stuck on net20 APIs - /// - internal class MSBuildTaskHostFileSystem : IFileSystem - { - private static readonly MSBuildTaskHostFileSystem Instance = new MSBuildTaskHostFileSystem(); - - public static MSBuildTaskHostFileSystem Singleton() => Instance; - - public bool FileOrDirectoryExists(string path) - { - return NativeMethodsShared.FileOrDirectoryExists(path); - } - - public FileAttributes GetAttributes(string path) - { - return File.GetAttributes(path); - } - - public DateTime GetLastWriteTimeUtc(string path) - { - return File.GetLastWriteTimeUtc(path); - } - - public bool DirectoryExists(string path) - { - return NativeMethodsShared.DirectoryExists(path); - } - - public IEnumerable EnumerateDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) - { - return Directory.GetDirectories(path, searchPattern, searchOption); - } - - public TextReader ReadFile(string path) - { - return new StreamReader(path); - } - - public Stream GetFileStream(string path, FileMode mode, FileAccess access, FileShare share) - { - return new FileStream(path, mode, access, share); - } - - public string ReadFileAllText(string path) - { - return File.ReadAllText(path); - } - - public byte[] ReadFileAllBytes(string path) - { - return File.ReadAllBytes(path); - } - - public IEnumerable EnumerateFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) - { - return Directory.GetFiles(path, searchPattern, searchOption); - } - - public IEnumerable EnumerateFileSystemEntries(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) - { - ErrorUtilities.VerifyThrow(searchOption == SearchOption.TopDirectoryOnly, $"In net20 {nameof(Directory.GetFileSystemEntries)} does not take a {nameof(SearchOption)} parameter"); - - return Directory.GetFileSystemEntries(path, searchPattern); - } - - public bool FileExists(string path) - { - return NativeMethodsShared.FileExists(path); - } - } -} diff --git a/src/MSBuildTaskHost/Immutable/ImmutableDictionary.cs b/src/MSBuildTaskHost/Immutable/ImmutableDictionary.cs deleted file mode 100644 index 025bb7c5536..00000000000 --- a/src/MSBuildTaskHost/Immutable/ImmutableDictionary.cs +++ /dev/null @@ -1,284 +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 System.Collections.Generic; -using System.Linq; - -#nullable disable - -namespace System.Collections.Immutable -{ - internal static class ImmutableExtensions - { - public static ImmutableDictionary ToImmutableDictionary(this IDictionary dictionary) - { - return new ImmutableDictionary(dictionary); - } - } - - internal static class ImmutableDictionary - { - internal static ImmutableDictionary Create(IEqualityComparer comparer) - { - return new ImmutableDictionary(comparer); - } - } - - /// - /// Inefficient ImmutableDictionary implementation: keep a mutable dictionary and wrap all operations. - /// - /// - /// - internal sealed class ImmutableDictionary : IDictionary, IDictionary - { - /// - /// The underlying dictionary. - /// - private Dictionary _backing; - - #region Read-only Operations - - public ICollection Keys => _backing.Keys; - public ICollection Values => _backing.Values; - - ICollection IDictionary.Keys => _backing.Keys; - ICollection IDictionary.Values => _backing.Values; - - public int Count => _backing.Count; - - public V this[K key] => _backing[key]; - - public bool IsReadOnly => true; - public bool IsFixedSize => true; - public bool IsSynchronized => true; - - public object SyncRoot => this; - - public bool TryGetValue(K key, out V value) - { - return _backing.TryGetValue(key, out value); - } - - public bool Contains(KeyValuePair item) - { - return _backing.Contains(item); - } - - bool IDictionary.Contains(object key) - { - return ((IDictionary)_backing).Contains(key); - } - - public bool ContainsKey(K key) - { - return _backing.ContainsKey(key); - } - - public IEnumerator> GetEnumerator() - { - return _backing.GetEnumerator(); - } - - IDictionaryEnumerator IDictionary.GetEnumerator() - { - return _backing.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _backing.GetEnumerator(); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - CheckCopyToArguments(array, arrayIndex); - foreach (var item in this) - { - array[arrayIndex++] = item; - } - } - - void ICollection.CopyTo(Array array, int arrayIndex) - { - CheckCopyToArguments(array, arrayIndex); - foreach (var item in this) - { - array.SetValue(new DictionaryEntry(item.Key, item.Value), arrayIndex++); - } - } - - private void CheckCopyToArguments(Array array, int arrayIndex) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - if (arrayIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - } - if (arrayIndex + Count > array.Length) - { - throw new ArgumentException(nameof(arrayIndex)); - } - } - - #endregion - - #region Write Operations - - internal ImmutableDictionary SetItem(K key, V value) - { - if (TryGetValue(key, out V existingValue) && Object.Equals(existingValue, value)) - { - return this; - } - - var clone = new ImmutableDictionary(_backing); - clone._backing[key] = value; - - return clone; - } - - internal ImmutableDictionary SetItems(IEnumerable> items) - { - var clone = new ImmutableDictionary(_backing); - foreach (KeyValuePair item in items) - { - clone._backing[item.Key] = item.Value; - } - - return clone; - } - - internal ImmutableDictionary Remove(K key) - { - if (!ContainsKey(key)) - { - return this; - } - - var clone = new ImmutableDictionary(_backing); - clone._backing.Remove(key); - - return clone; - } - - internal ImmutableDictionary Clear() - { - return new ImmutableDictionary(_backing.Comparer); - } - - internal ImmutableDictionary() - { - _backing = new Dictionary(); - } - - internal ImmutableDictionary(IEqualityComparer comparer) - { - _backing = new Dictionary(comparer); - } - - internal ImmutableDictionary(IDictionary source, IEqualityComparer keyComparer = null) - { - if (source is ImmutableDictionary imm) - { - _backing = new Dictionary(imm._backing, keyComparer ?? imm._backing.Comparer); - } - else - { - _backing = new Dictionary(source, keyComparer); - } - } - - internal static ImmutableDictionary Empty - { - get - { - return new ImmutableDictionary(); - } - } - - public IEqualityComparer KeyComparer { get => _backing.Comparer; internal set => throw new NotSupportedException(); } - - internal KeyValuePair[] ToArray() - { - return _backing.ToArray(); - } - - internal ImmutableDictionary AddRange(KeyValuePair[] v) - { - var n = new Dictionary(_backing, _backing.Comparer); - - foreach (var item in v) - { - n.Add(item.Key, item.Value); - } - - return new ImmutableDictionary(n); - } - - internal ImmutableDictionary WithComparers(IEqualityComparer keyComparer) - { - return new ImmutableDictionary(_backing, keyComparer); - } - - #endregion - - #region Unsupported Operations - - object IDictionary.this[object key] - { - get { return _backing[(K)key]; } - set { throw new NotSupportedException(); } - } - - void IDictionary.Add(object key, object value) - { - throw new NotSupportedException(); - } - - void IDictionary.Remove(object key) - { - throw new NotSupportedException(); - } - - void IDictionary.Clear() - { - throw new NotSupportedException(); - } - - V IDictionary.this[K key] - { - get { return _backing[key]; } - set { throw new NotSupportedException(); } - } - - void IDictionary.Add(K key, V value) - { - throw new NotSupportedException(); - } - - bool IDictionary.Remove(K key) - { - throw new NotSupportedException(); - } - - void ICollection>.Add(KeyValuePair item) - { - throw new NotSupportedException(); - } - - void ICollection>.Clear() - { - throw new NotSupportedException(); - } - - bool ICollection>.Remove(KeyValuePair item) - { - throw new NotSupportedException(); - } - - #endregion - } -} diff --git a/src/MSBuildTaskHost/LoadedType.cs b/src/MSBuildTaskHost/LoadedType.cs new file mode 100644 index 00000000000..456193f1887 --- /dev/null +++ b/src/MSBuildTaskHost/LoadedType.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost; + +/// +/// This class packages information about a type loaded from an assembly: for example, +/// the GenerateResource task class type or the ConsoleLogger logger class type. +/// +internal sealed class LoadedType +{ + /// + /// Creates an instance of this class for the given type. + /// + /// The Type to be loaded + /// The assembly file path used to load the assembly. + /// The assembly which has been loaded, if any. + internal LoadedType(Type type, string assemblyFilePath, Assembly loadedAssembly) + { + ErrorUtilities.VerifyThrow(type != null, "We must have the type."); + ErrorUtilities.VerifyThrow(assemblyFilePath != null, "We must have the assembly file path the type was loaded from."); + ErrorUtilities.VerifyThrow(loadedAssembly is not null, "The assembly should always be loaded even if only by MetadataLoadContext."); + + Type = type; + AssemblyFilePath = assemblyFilePath; + + LoadedAssemblyName = loadedAssembly.GetName(); + + // For inline tasks loaded from bytes, Assembly.Location is empty, so use the original path + Path = string.IsNullOrEmpty(loadedAssembly.Location) + ? assemblyFilePath + : loadedAssembly.Location; + + LoadedAssembly = loadedAssembly; + HasLoadInSeparateAppDomainAttribute = Type.IsDefined(typeof(LoadInSeparateAppDomainAttribute), inherit: true); + IsMarshalByRef = Type.IsMarshalByRef; + } + + /// + /// Gets whether there's a LoadInSeparateAppDomain attribute on this type. + /// + public bool HasLoadInSeparateAppDomainAttribute { get; } + + /// + /// Gets whether this type implements MarshalByRefObject. + /// + public bool IsMarshalByRef { get; } + + /// + /// Gets the type that was loaded from an assembly. + /// + /// The loaded type. + internal Type Type { get; } + + internal AssemblyName LoadedAssemblyName { get; } + + internal string Path { get; } + + /// + /// If we loaded an assembly for this type. + /// We use this information to help created AppDomains to resolve types that it could not load successfully. + /// + internal Assembly LoadedAssembly { get; } + + internal string AssemblyFilePath { get; } +} diff --git a/src/MSBuildTaskHost/MSBuild.ico b/src/MSBuildTaskHost/MSBuild.ico new file mode 100644 index 00000000000..f70202a070e Binary files /dev/null and b/src/MSBuildTaskHost/MSBuild.ico differ diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj index 26f9dead650..56f0e0b4b02 100644 --- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj +++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -1,4 +1,4 @@ - + @@ -16,256 +16,49 @@ win7-x86;win7-x64 true - false - $(DefineConstants);CLR2COMPATIBILITY;TASKHOST;NO_FRAMEWORK_IVT - + Microsoft.Build.TaskHost true + true - ..\MSBuild\MSBuild.ico + MSBuild.ico true - full + + full + + + $(DefineConstants);NET20 + + + + + + true - - - - GlobalUsings.cs - - - BuildEnvironmentHelper.cs - - - BuildEnvironmentState.cs - - - AssemblyNameComparer.cs - - - BuildEngineResult.cs - - - IBuildEngine3.cs - - - RunInSTAAtribute.cs - - - ITaskItem2.cs - - - IExtendedBuildEventArgs.cs - - - - - - - - - CopyOnWriteDictionary.cs - - - - - - - ErrorUtilities.cs - - - SharedErrorUtilities.cs - - - EscapingUtilities.cs - - - ExceptionHandling.cs - - - FileUtilities.cs - - - SharedFileUtilities.cs - - - FileUtilitiesRegex.cs - - - INodeEndpoint.cs - - - INodePacket.cs - - - INodePacketFactory.cs - - - INodePacketHandler.cs - - - ITranslatable.cs - - - ITranslator.cs - - - - InternalErrorException.cs - - - InterningBinaryReader.cs - - - BinaryReaderFactory.cs - - - BinaryReaderExtensions.cs - - - BinaryWriterExtensions.cs - - - LogMessagePacketBase.cs - - - Modifiers.cs - - - - NativeMethodsShared.cs - - - AnsiDetector.cs - - - NodeBuildComplete.cs - - - NodeEndpointOutOfProcBase.cs - - - NodeEngineShutdownReason.cs - - - NodePacketFactory.cs - - - BinaryTranslator.cs - - - BuildExceptionBase.cs - - - BuildExceptionRemoteState.cs - - - BuildExceptionSerializationHelper.cs - - - GenericBuildTransferredException.cs - - - NodeShutdown.cs - - - ReadOnlyEmptyCollection.cs - - - ResourceUtilities.cs - - - StringBuilderCache.cs - - - SupportedOSPlatform.cs - - - TaskEngineAssemblyResolver.cs - - - TaskParameterTypeVerifier.cs - - - FrameworkTraits.cs - - - VisualStudioLocationHelper.cs - - - XMakeAttributes.cs - - - - - ChangeWaves.cs - - - - - - - - - - - - - OutOfProcTaskHostTaskResult.cs - - - - - - - - - - - - - - - - - - - - - - - - - OutOfProcTaskAppDomainWrapperBase.cs - - - - - - - - - $(AssemblyName).Strings.shared.resources - Designer - - - - - + + + + + true + + + diff --git a/src/MSBuildTaskHost/OutOfProcTaskAppDomainWrapper.cs b/src/MSBuildTaskHost/OutOfProcTaskAppDomainWrapper.cs index 442522f743b..315b9848d7e 100644 --- a/src/MSBuildTaskHost/OutOfProcTaskAppDomainWrapper.cs +++ b/src/MSBuildTaskHost/OutOfProcTaskAppDomainWrapper.cs @@ -2,29 +2,234 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.BackEnd; +using Microsoft.Build.TaskHost.Utilities; -#nullable disable +namespace Microsoft.Build.TaskHost; -namespace Microsoft.Build.CommandLine +/// +/// Class for executing a task in an AppDomain. +/// +internal sealed class OutOfProcTaskAppDomainWrapper : IDisposable { + private const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance; + + /// + /// This is an appDomain instance if any is created for running this task. + /// + private AppDomain? _taskAppDomain; + + /// + /// This is responsible for invoking Execute on the Task + /// Any method calling must remember to call . + /// + /// + /// We also allow the Task to have a reference to the BuildEngine by design + /// at ITask.BuildEngine. + /// + /// The to use. + /// The name of the task to be executed. + /// The path of the task binary. + /// The path to the project file in which the task invocation is located. + /// The line in the project file where the task invocation is located. + /// The column in the project file where the task invocation is located. + /// The that we want to use to launch AppDomain-isolated tasks. + /// Parameters that will be passed to the task when created. + /// Task completion result showing success, failure or if there was a crash. + public OutOfProcTaskHostTaskResult ExecuteTask( + IBuildEngine buildEngine, + string taskName, + string taskLocation, + string taskFile, + int taskLine, + int taskColumn, + AppDomainSetup appDomainSetup, + Dictionary taskParameters) + { + _taskAppDomain = null; + + LoadedType? taskType; + try + { + TypeLoader typeLoader = new(TaskLoader.IsTaskClass); + taskType = typeLoader.Load(taskName, taskLocation); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + return OutOfProcTaskHostTaskResult.CrashedDuringInitialization( + GetRelevantException(e), + "TaskInstantiationFailureError", + [taskName, taskLocation, string.Empty]); + } + + return InstantiateAndExecuteTask( + buildEngine, + taskType!, + taskName, + taskLocation, + taskFile, + taskLine, + taskColumn, + appDomainSetup, + taskParameters); + } + /// - /// Class for executing a task in an AppDomain + /// This is responsible for cleaning up the task after the OutOfProcTaskHostNode has gathered everything it needs from this execution + /// For example: We will need to hold on new AppDomains created until we finish getting all outputs from the task + /// Add any other cleanup tasks here. Any method calling ExecuteTask must remember to call CleanupTask. /// - [Serializable] - internal class OutOfProcTaskAppDomainWrapper : OutOfProcTaskAppDomainWrapperBase + public void Dispose() + { + if (_taskAppDomain != null) + { + AppDomain.Unload(_taskAppDomain); + } + + TaskLoader.RemoveAssemblyResolver(); + } + + /// + /// Do the work of actually instantiating and running the task. + /// + private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask( + IBuildEngine buildEngine, + LoadedType taskType, + string taskName, + string taskLocation, + string taskFile, + int taskLine, + int taskColumn, + AppDomainSetup appDomainSetup, + Dictionary taskParameters) + { + _taskAppDomain = null; + ITask wrappedTask; + + try + { + wrappedTask = TaskLoader.CreateTask( + taskType, + taskName, + taskFile, + taskLine, + taskColumn, + LogErrorDelegate, + appDomainSetup, + appDomainCreated: null, // custom app domain assembly loading won't be available for task host + isOutOfProc: true, + out _taskAppDomain)!; + + wrappedTask.BuildEngine = buildEngine; + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + return OutOfProcTaskHostTaskResult.CrashedDuringInitialization( + GetRelevantException(e), + resourceId: "TaskInstantiationFailureError", + resourceArgs: [taskName, taskLocation, string.Empty]); + } + + if (TryAssignInputs(wrappedTask, taskName, taskParameters) is { } result) + { + return result; + } + + bool success = false; + try + { + // If it didn't crash and return before now, we're clear to go ahead and execute here. + success = wrappedTask.Execute(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + return OutOfProcTaskHostTaskResult.CrashedDuringExecution(e); + } + + Dictionary? finalParameterValues = CollectOutputs(wrappedTask); + + return success + ? OutOfProcTaskHostTaskResult.Success(finalParameterValues) + : OutOfProcTaskHostTaskResult.Failure(finalParameterValues); + + void LogErrorDelegate(string taskLocation, int taskLine, int taskColumn, string message) + { + BuildErrorEventArgs error = new( + subcategory: null, + code: null, + file: taskLocation, + lineNumber: taskLine, + columnNumber: taskColumn, + endLineNumber: 0, + endColumnNumber: 0, + message, + helpKeyword: null, + senderName: taskName); + + buildEngine.LogErrorEvent(error); + } + } + + private static OutOfProcTaskHostTaskResult? TryAssignInputs( + ITask wrappedTask, string taskName, Dictionary taskParameters) { - /// - /// This is a stub for CLR2 in place of the OutOfProcTaskAppDomainWrapper class - /// as used in CLR4 to support cancellation of ICancelable tasks. - /// We provide a stub for CancelTask here so that the OutOfProcTaskHostNode - /// that's shared by both the MSBuild.exe and MSBuildTaskHost.exe, - /// can safely allow MSBuild.exe CLR4 Out-Of-Proc Task Host to call ICancelableTask.Cancel() - /// - /// False - Used by the OutOfProcTaskHostNode to determine if the task is ICancelable - internal bool CancelTask() - { - // This method is a stub we will not do anything here. - return false; + Type wrappedTaskType = wrappedTask.GetType(); + + foreach (KeyValuePair kvp in taskParameters) + { + string name = kvp.Key; + TaskParameter parameter = kvp.Value; + + try + { + PropertyInfo property = wrappedTaskType.GetProperty(name, PublicInstance); + property.SetValue(wrappedTask, parameter?.WrappedParameter, index: null); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + return OutOfProcTaskHostTaskResult.CrashedDuringInitialization( + GetRelevantException(e), + resourceId: "InvalidTaskAttributeError", + resourceArgs: [name, parameter?.ToString() ?? string.Empty, taskName]); + } } + + return null; } + + private static Dictionary? CollectOutputs(ITask wrappedTask) + { + Type wrappedTaskType = wrappedTask.GetType(); + + Dictionary? outputs = null; + + foreach (PropertyInfo property in wrappedTaskType.GetProperties(PublicInstance)) + { + // only record outputs + if (property.GetCustomAttributes(typeof(OutputAttribute), inherit: true).Length > 0) + { + outputs ??= new(StringComparer.OrdinalIgnoreCase); + + try + { + outputs[property.Name] = property.GetValue(wrappedTask, index: null); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // If it's not a critical exception, we assume there's some sort of problem in the property getter. + // So, save the exception and we'll re-throw once we're back on the main node side of the + // communications pipe. + outputs[property.Name] = e; + } + } + } + + return outputs; + } + + private static Exception GetRelevantException(Exception e) + => e is TargetInvocationException ? e.InnerException : e; } diff --git a/src/MSBuildTaskHost/OutOfProcTaskHost.cs b/src/MSBuildTaskHost/OutOfProcTaskHost.cs index 9ee7b001bca..a0422afb7f0 100644 --- a/src/MSBuildTaskHost/OutOfProcTaskHost.cs +++ b/src/MSBuildTaskHost/OutOfProcTaskHost.cs @@ -3,133 +3,130 @@ using System; using System.Diagnostics; - -// CR: We could move MSBuildApp.ExitType out of MSBuildApp -using Microsoft.Build.Execution; -using Microsoft.Build.Shared; - -#nullable disable - -namespace Microsoft.Build.CommandLine +using Microsoft.Build.TaskHost.BackEnd; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost; + +/// +/// This is the Out-Of-Proc Task Host for supporting Cross-Targeting tasks. +/// +/// +/// It will be responsible for: +/// - Task execution +/// - Communicating with the MSBuildApp process, specifically the TaskHostFactory +/// (Logging messages, receiving Tasks from TaskHostFactory, sending results and other messages). +/// +public static class OutOfProcTaskHost { /// - /// This is the Out-Of-Proc Task Host for supporting Cross-Targeting tasks. + /// Enumeration of the various ways in which the MSBuildTaskHost.exe application can exit. /// - /// - /// It will be responsible for: - /// - Task execution - /// - Communicating with the MSBuildApp process, specifically the TaskHostFactory - /// (Logging messages, receiving Tasks from TaskHostFactory, sending results and other messages) - /// - public static class OutOfProcTaskHost + internal enum ExitType { /// - /// Enumeration of the various ways in which the MSBuildTaskHost.exe application can exit. + /// The application executed successfully. /// - internal enum ExitType - { - /// - /// The application executed successfully. - /// - Success, - - /// - /// We received a request from MSBuild.exe to terminate - /// - TerminateRequest, - - /// - /// A logger aborted the build. - /// - LoggerAbort, - - /// - /// A logger failed unexpectedly. - /// - LoggerFailure, - - /// - /// The Task Host Node did not terminate gracefully - /// - TaskHostNodeFailed, - - /// - /// An unexpected failure - /// - Unexpected - } + Success, /// - /// Main Entry Point + /// We received a request from MSBuild.exe to terminate. /// - /// - /// We won't execute any tasks in the main thread, so we don't need to be in an STA - /// - [MTAThread] - public static int Main() - { - int exitCode = Execute() == ExitType.Success ? 0 : 1; - return exitCode; - } + TerminateRequest, + + /// + /// A logger aborted the build. + /// + LoggerAbort, /// - /// Orchestrates the execution of the application. - /// Also responsible for top-level error handling. + /// A logger failed unexpectedly. /// - /// - /// A value of Success if the bootstrapping succeeds - /// - internal static ExitType Execute() + LoggerFailure, + + /// + /// The Task Host Node did not terminate gracefully. + /// + TaskHostNodeFailed, + + /// + /// An unexpected failure. + /// + Unexpected + } + + /// + /// Main Entry Point. + /// + /// + /// We won't execute any tasks in the main thread, so we don't need to be in an STA. + /// + // UNDONE: Setting [MTAThread] is almost certainly incorrect for MSBuildTaskHost. + // Prior to .NET Framework 4.0, all of MSBuild ran in an STA. However, the change + // that makes MSBuildTaskHost run in an MTA was made over 10 years ago. + [MTAThread] + public static int Main() + => Execute() == ExitType.Success ? 0 : 1; + + /// + /// Orchestrates the execution of the application. + /// Also responsible for top-level error handling. + /// + /// + /// A value of Success if the bootstrapping succeeds. + /// + internal static ExitType Execute() + { + switch (Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART")) { - switch (Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART")) - { + #if FEATURE_DEBUG_LAUNCH - case "1": - Debugger.Launch(); - break; + case "1": + Debugger.Launch(); + break; #endif - case "2": - // Sometimes easier to attach rather than deal with JIT prompt - Console.WriteLine($"Waiting for debugger to attach ({EnvironmentUtilities.ProcessPath} PID {EnvironmentUtilities.CurrentProcessId}). Press enter to continue..."); - Console.ReadLine(); - break; - case "3": - // Value "3" skips debugging for TaskHost processes but debugs the main MSBuild process - // This is useful when you want to debug MSBuild but not the child TaskHost processes - break; - } + case "2": + // Sometimes easier to attach rather than deal with JIT prompt + Console.WriteLine($"Waiting for debugger to attach ({EnvironmentUtilities.ProcessPath} PID {EnvironmentUtilities.CurrentProcessId}). Press enter to continue..."); - bool restart = false; - do - { - OutOfProcTaskHostNode oopTaskHostNode = new OutOfProcTaskHostNode(); - Exception taskHostShutDownException = null; - NodeEngineShutdownReason taskHostShutDownReason = oopTaskHostNode.Run(out taskHostShutDownException); + Console.ReadLine(); + break; - if (taskHostShutDownException != null) - { - return ExitType.TaskHostNodeFailed; - } - - switch (taskHostShutDownReason) - { - case NodeEngineShutdownReason.BuildComplete: - return ExitType.Success; + case "3": + // Value "3" skips debugging for TaskHost processes but debugs the main MSBuild process + // This is useful when you want to debug MSBuild but not the child TaskHost processes + break; + } - case NodeEngineShutdownReason.BuildCompleteReuse: - restart = true; - break; + bool restart = false; + do + { + var oopTaskHostNode = new OutOfProcTaskHostNode(); + NodeEngineShutdownReason taskHostShutDownReason = oopTaskHostNode.Run(out Exception? taskHostShutDownException); - default: - return ExitType.TaskHostNodeFailed; - } + if (taskHostShutDownException != null) + { + return ExitType.TaskHostNodeFailed; } - while (restart); - // Should not happen - ErrorUtilities.ThrowInternalErrorUnreachable(); - return ExitType.Unexpected; + switch (taskHostShutDownReason) + { + case NodeEngineShutdownReason.BuildComplete: + return ExitType.Success; + + case NodeEngineShutdownReason.BuildCompleteReuse: + restart = true; + break; + + default: + return ExitType.TaskHostNodeFailed; + } } + while (restart); + + // Should not happen + ErrorUtilities.ThrowInternalErrorUnreachable(); + return ExitType.Unexpected; } } diff --git a/src/MSBuildTaskHost/OutOfProcTaskHostNode.cs b/src/MSBuildTaskHost/OutOfProcTaskHostNode.cs new file mode 100644 index 00000000000..1c853ff09e0 --- /dev/null +++ b/src/MSBuildTaskHost/OutOfProcTaskHostNode.cs @@ -0,0 +1,926 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Remoting; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.BackEnd; +using Microsoft.Build.TaskHost.Collections; +using Microsoft.Build.TaskHost.Resources; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost; + +/// +/// This class represents an implementation of INode for out-of-proc node for hosting tasks. +/// +internal class OutOfProcTaskHostNode : INodePacketFactory, INodePacketHandler, IBuildEngine2 +{ + /// + /// Keeps a record of all environment variables that, on startup of the task host, have a different + /// value from those that are passed to the task host in the configuration packet for the first task. + /// These environments are assumed to be effectively identical, so the only difference between the + /// two sets of values should be any environment variables that differ between e.g. a 32-bit and a 64-bit + /// process. Those are the variables that this dictionary should store. + /// + /// - The key into the dictionary is the name of the environment variable. + /// - The Key of the KeyValuePair is the value of the variable in the parent process -- the value that we + /// wish to ensure is replaced by whatever the correct value in our current process is. + /// - The Value of the KeyValuePair is the value of the variable in the current process -- the value that + /// we wish to replay the Key value with in the environment that we receive from the parent before + /// applying it to the current process. + /// + /// Note that either value in the KeyValuePair can be null, as it is completely possible to have an + /// environment variable that is set in 32-bit processes but not in 64-bit, or vice versa. + /// + /// This dictionary must be static because otherwise, if a node is sitting around waiting for reuse, it will + /// have inherited the environment from the previous build, and any differences between the two will be seen + /// as "legitimate". There is no way for us to know what the differences between the startup environment of + /// the previous build and the environment of the first task run in the task host in this build -- so we + /// must assume that the 4ish system environment variables that this is really meant to catch haven't + /// somehow magically changed between two builds spaced no more than 15 minutes apart. + /// + private static Dictionary>? s_mismatchedEnvironmentValues; + + /// + /// The endpoint used to talk to the host. + /// + private NodeEndpointOutOfProcTaskHost? _nodeEndpoint; + + /// + /// The packet factory. + /// + private readonly NodePacketFactory _packetFactory; + + /// + /// The event which is set when we receive packets. + /// + private readonly AutoResetEvent _packetReceivedEvent; + + /// + /// The queue of packets we have received but which have not yet been processed. + /// + private readonly Queue _receivedPackets; + + /// + /// The current configuration for this task host. + /// + private TaskHostConfiguration? _currentConfiguration; + + /// + /// The saved environment for the process. + /// + private Dictionary? _savedEnvironment; + + /// + /// The event which is set when we should shut down. + /// + private readonly ManualResetEvent _shutdownEvent; + + /// + /// The reason we are shutting down. + /// + private NodeEngineShutdownReason _shutdownReason; + + /// + /// We set this flag to track a currently executing task. + /// + private bool _isTaskExecuting; + + /// + /// The event which is set when a task has completed. + /// + private readonly AutoResetEvent _taskCompleteEvent; + + /// + /// Packet containing all the information relating to the + /// completed state of the task. + /// + private TaskHostTaskComplete? _taskCompletePacket; + + /// + /// Object used to synchronize access to taskCompletePacket. + /// + private readonly object _taskCompleteLock = new(); + + /// + /// The event which is set when a task is cancelled. + /// + private readonly ManualResetEvent _taskCancelledEvent; + + /// + /// The thread currently executing user task in the TaskRunner. + /// + private Thread? _taskRunnerThread; + + /// + /// This is the wrapper for the user task to be executed. + /// We are providing a wrapper to create a possibility of executing the task in a separate AppDomain. + /// + private OutOfProcTaskAppDomainWrapper? _taskWrapper; + + /// + /// Flag indicating if we should debug communications or not. + /// + private bool _debugCommunications; + + /// + /// Flag indicating whether we should modify the environment based on any differences we find between that of the + /// task host at startup and the environment passed to us in our initial task configuration packet. + /// + private bool _updateEnvironment; + + /// + /// An interim step between MSBuildTaskHostDoNotUpdateEnvironment=1 and the default update behavior: go ahead and + /// do all the updates that we would otherwise have done by default, but log any updates that are made (at low + /// importance) so that the user is aware. + /// + private bool _updateEnvironmentAndLog; + + public OutOfProcTaskHostNode() + { + // We don't know what the current build thinks this variable should be until RunTask(), but as a fallback in case there are + // communications before we get the configuration set up, just go with what was already in the environment from when this node + // was initially launched. + _debugCommunications = Traits.Instance.DebugNodeCommunication; + + _receivedPackets = new Queue(); + + // These WaitHandles are disposed in HandleShutDown() + _packetReceivedEvent = new AutoResetEvent(false); + _shutdownEvent = new ManualResetEvent(false); + _taskCompleteEvent = new AutoResetEvent(false); + _taskCancelledEvent = new ManualResetEvent(false); + + _packetFactory = new NodePacketFactory(); + + RegisterPacketHandler(NodePacketType.TaskHostConfiguration, TaskHostConfiguration.FactoryForDeserialization, this); + RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this); + RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); + } + + /// + /// Returns the value of ContinueOnError for the currently executing task. + /// + bool IBuildEngine.ContinueOnError + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ContinueOnError; + } + } + + /// + /// Returns the line number of the location in the project file of the currently executing task. + /// + public int LineNumberOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.LineNumberOfTask; + } + } + + /// + /// Returns the column number of the location in the project file of the currently executing task. + /// + public int ColumnNumberOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ColumnNumberOfTask; + } + } + + /// + /// Returns the project file of the currently executing task. + /// + public string ProjectFileOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ProjectFileOfTask; + } + } + + /// + /// Stub implementation of IBuildEngine2.IsRunningMultipleNodes. The task host does not support this sort of + /// IBuildEngine callback, so error. + /// + bool IBuildEngine2.IsRunningMultipleNodes + { + get + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + } + + /// + /// Sends the provided error back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogErrorEvent(BuildErrorEventArgs e) + => SendBuildEvent(e); + + /// + /// Sends the provided warning back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogWarningEvent(BuildWarningEventArgs e) + => SendBuildEvent(e); + + /// + /// Sends the provided message back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogMessageEvent(BuildMessageEventArgs e) + => SendBuildEvent(e); + + /// + /// Sends the provided custom event back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogCustomEvent(CustomBuildEventArgs e) + => SendBuildEvent(e); + + /// + /// Stub implementation of IBuildEngine.BuildProjectFile. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + bool IBuildEngine.BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + + /// + /// Stub implementation of IBuildEngine2.BuildProjectFile. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + bool IBuildEngine2.BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) + { + LogErrorFromResource(SR.BuildEngineCallbacksInTaskHostUnsupported); + return false; + } + + /// + /// Stub implementation of IBuildEngine2.BuildProjectFilesInParallel. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + bool IBuildEngine2.BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) + { + LogErrorFromResource(SR.BuildEngineCallbacksInTaskHostUnsupported); + return false; + } + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + => _packetFactory.RegisterPacketHandler(packetType, factory, handler); + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + => _packetFactory.UnregisterPacketHandler(packetType); + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) + => _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + + /// + /// Takes a serializer and deserializes the packet. + /// + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) + => _packetFactory.DeserializePacket(packetType, translator); + + /// + /// Routes the specified packet. + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + => _packetFactory.RoutePacket(nodeId, packet); + + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + public void PacketReceived(int node, INodePacket packet) + { + lock (_receivedPackets) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + } + + /// + /// Starts up the node and processes messages until the node is requested to shut down. + /// + /// The exception which caused shutdown, if any. + /// The reason for shutting down. + public NodeEngineShutdownReason Run(out Exception? shutdownException, byte parentPacketVersion = 1) + { + shutdownException = null; + + // Snapshot the current environment + _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + + _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(parentPacketVersion); + _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged); + _nodeEndpoint.Listen(this); + + WaitHandle[] waitHandles = [_shutdownEvent, _packetReceivedEvent, _taskCompleteEvent, _taskCancelledEvent]; + + while (true) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: // shutdownEvent + NodeEngineShutdownReason shutdownReason = HandleShutdown(); + return shutdownReason; + + case 1: // packetReceivedEvent + int packetCount = _receivedPackets.Count; + + while (packetCount > 0) + { + INodePacket? packet = null; + + lock (_receivedPackets) + { + if (_receivedPackets.Count > 0) + { + packet = _receivedPackets.Dequeue(); + } + else + { + break; + } + } + + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + + case 2: // taskCompleteEvent + CompleteTask(); + break; + + case 3: // taskCancelledEvent + CancelTask(); + break; + } + } + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.TaskHostConfiguration: + HandleTaskHostConfiguration((TaskHostConfiguration)packet); + break; + + case NodePacketType.TaskHostTaskCancelled: + _taskCancelledEvent.Set(); + break; + + case NodePacketType.NodeBuildComplete: + HandleNodeBuildComplete((NodeBuildComplete)packet); + break; + } + } + + /// + /// Configure the task host according to the information received in the configuration packet. + /// + private void HandleTaskHostConfiguration(TaskHostConfiguration taskHostConfiguration) + { + ErrorUtilities.VerifyThrow(!_isTaskExecuting, "Why are we getting a TaskHostConfiguration packet while we're still executing a task?"); + _currentConfiguration = taskHostConfiguration; + + // Kick off the task running thread. + _taskRunnerThread = new Thread(new ParameterizedThreadStart(RunTask)) + { + Name = "Task runner for task " + taskHostConfiguration.TaskName + }; + + _taskRunnerThread.Start(taskHostConfiguration); + } + + /// + /// The task has been completed. + /// + private void CompleteTask() + { + ErrorUtilities.VerifyThrow(!_isTaskExecuting, "The task should be done executing before CompleteTask."); + ErrorUtilities.VerifyThrow(_nodeEndpoint != null, $"{nameof(_nodeEndpoint)} is null."); + + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + TaskHostTaskComplete taskCompletePacketToSend; + + lock (_taskCompleteLock) + { + ErrorUtilities.VerifyThrowInternalNull(_taskCompletePacket, "taskCompletePacket"); + taskCompletePacketToSend = _taskCompletePacket; + _taskCompletePacket = null; + } + + _nodeEndpoint.SendData(taskCompletePacketToSend); + } + + _currentConfiguration = null; + + // If the task has been canceled, the event will still be set. + // If so, now that we've completed the task, we want to shut down + // this node -- with no reuse, since we don't know whether the + // task we canceled left the node in a good state or not. + if (_taskCancelledEvent.WaitOne(0)) + { + _shutdownReason = NodeEngineShutdownReason.BuildComplete; + _shutdownEvent.Set(); + } + } + + /// + /// This task has been cancelled. Attempt to cancel the task. + /// + private void CancelTask() + { + // Create a possibility for the task to be aborted if the user really wants it dropped dead asap + if (Environment.GetEnvironmentVariable("MSBUILDTASKHOSTABORTTASKONCANCEL") == "1") + { + // Don't bother aborting the task if it has passed the actual user task Execute() + // It means we're already in the process of shutting down - Wait for the taskCompleteEvent to be set instead. + if (_isTaskExecuting) + { + // The thread will be terminated crudely so our environment may be trashed but it's ok since we are + // shutting down ASAP. + _taskRunnerThread?.Abort(); + } + } + } + + /// + /// Handles the NodeBuildComplete packet. + /// + private void HandleNodeBuildComplete(NodeBuildComplete buildComplete) + { + ErrorUtilities.VerifyThrow(!_isTaskExecuting, "We should never have a task in the process of executing when we receive NodeBuildComplete."); + + // TaskHostNodes lock assemblies with custom tasks produced by build scripts if NodeReuse is on. This causes failures if the user builds twice. + _shutdownReason = buildComplete.PrepareForReuse && Traits.Instance.EscapeHatches.ReuseTaskHostNodes + ? NodeEngineShutdownReason.BuildCompleteReuse + : NodeEngineShutdownReason.BuildComplete; + + _shutdownEvent.Set(); + } + + /// + /// Perform necessary actions to shut down the node. + /// + private NodeEngineShutdownReason HandleShutdown() + { + ErrorUtilities.VerifyThrow(_nodeEndpoint != null, $"{nameof(_nodeEndpoint)} is null."); + + // Wait for the RunTask task runner thread before shutting down so that we can cleanly dispose all WaitHandles. + _taskRunnerThread?.Join(); + + using StreamWriter? debugWriter = _debugCommunications + ? File.CreateText(Path.Combine(FileUtilities.TempFileDirectory, $"MSBuild_NodeShutdown_{EnvironmentUtilities.CurrentProcessId}.txt")) + : null; + + debugWriter?.WriteLine("Node shutting down with reason {0}.", _shutdownReason); + + // On Windows, a process holds a handle to the current directory, + // so reset it away from a user-requested folder that may get deleted. + NativeMethods.SetCurrentDirectory(FileUtilities.MSBuildTaskHostDirectory); + + // Restore the original environment, best effort. + try + { + CommunicationsUtilities.SetEnvironment(_savedEnvironment); + } + catch (Exception ex) + { + debugWriter?.WriteLine("Failed to restore the original environment: {0}.", ex); + } + + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + // Notify the BuildManager that we are done. + _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested)); + + // Flush all packets to the pipe and close it down. This blocks until the shutdown is complete. + _nodeEndpoint.OnLinkStatusChanged -= new LinkStatusChangedDelegate(OnLinkStatusChanged); + } + + _nodeEndpoint.Disconnect(); + + // Dispose these WaitHandles + _packetReceivedEvent.Close(); + _shutdownEvent.Close(); + _taskCompleteEvent.Close(); + _taskCancelledEvent.Close(); + + return _shutdownReason; + } + + /// + /// Event handler for the node endpoint's LinkStatusChanged event. + /// + private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + switch (status) + { + case LinkStatus.ConnectionFailed: + case LinkStatus.Failed: + _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + _shutdownEvent.Set(); + break; + + case LinkStatus.Inactive: + break; + + default: + break; + } + } + + /// + /// Task runner method. + /// + private void RunTask(object state) + { + _isTaskExecuting = true; + OutOfProcTaskHostTaskResult? taskResult = null; + TaskHostConfiguration taskConfiguration = (TaskHostConfiguration)state; + + // We only really know the values of these variables for sure once we see what we received from our parent + // environment -- otherwise if this was a completely new build, we could lose out on expected environment + // variables. + _debugCommunications = taskConfiguration.BuildProcessEnvironment.HasValue("MSBUILDDEBUGCOMM", "1", StringComparison.OrdinalIgnoreCase); + _updateEnvironment = !taskConfiguration.BuildProcessEnvironment.HasValue("MSBuildTaskHostDoNotUpdateEnvironment", "1", StringComparison.OrdinalIgnoreCase); + _updateEnvironmentAndLog = taskConfiguration.BuildProcessEnvironment.HasValue("MSBuildTaskHostUpdateEnvironmentAndLog", "1", StringComparison.OrdinalIgnoreCase); + + try + { + // Change to the startup directory + NativeMethods.SetCurrentDirectory(taskConfiguration.StartupDirectory); + + if (_updateEnvironment) + { + InitializeMismatchedEnvironmentTable(taskConfiguration.BuildProcessEnvironment); + } + + // Now set the new environment + SetTaskHostEnvironment(taskConfiguration.BuildProcessEnvironment); + + // Set culture + Thread.CurrentThread.CurrentCulture = taskConfiguration.Culture; + Thread.CurrentThread.CurrentUICulture = taskConfiguration.UICulture; + + // We will not create an appdomain now because of a bug + // As a fix, we will create the class directly without wrapping it in a domain + _taskWrapper = new OutOfProcTaskAppDomainWrapper(); + + taskResult = _taskWrapper.ExecuteTask( + buildEngine: this, + taskConfiguration.TaskName, + taskConfiguration.TaskLocation, + taskConfiguration.ProjectFileOfTask, + taskConfiguration.LineNumberOfTask, + taskConfiguration.ColumnNumberOfTask, + taskConfiguration.AppDomainSetup, + taskConfiguration.TaskParameters); + } + catch (ThreadAbortException) + { + // This thread was aborted as part of Cancellation, we will return a failure task result + taskResult = OutOfProcTaskHostTaskResult.Failure(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + taskResult = OutOfProcTaskHostTaskResult.CrashedDuringExecution(e); + } + finally + { + try + { + _isTaskExecuting = false; + + Dictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + currentEnvironment = UpdateEnvironmentForMainNode(currentEnvironment); + + taskResult ??= OutOfProcTaskHostTaskResult.Failure(); + + lock (_taskCompleteLock) + { + _taskCompletePacket = new TaskHostTaskComplete(taskResult, currentEnvironment); + } + + foreach (TaskParameter param in taskConfiguration.TaskParameters.Values) + { + // Tell remoting to forget connections to the parameter + RemotingServices.Disconnect(param); + } + + // Restore the original clean environment + CommunicationsUtilities.SetEnvironment(_savedEnvironment); + } + catch (Exception e) + { + lock (_taskCompleteLock) + { + // Create a minimal taskCompletePacket to carry the exception so that the TaskHostTask does not hang while waiting + _taskCompletePacket = new TaskHostTaskComplete( + OutOfProcTaskHostTaskResult.CrashedAfterExecution(e), + buildProcessEnvironment: null); + } + } + finally + { + // Call Dispose to unload any AppDomains and other necessary cleanup in the taskWrapper + _taskWrapper?.Dispose(); + + // The task has now fully completed executing + _taskCompleteEvent.Set(); + } + } + } + + /// + /// Set the environment for the task host -- includes possibly munging the given + /// environment somewhat to account for expected environment differences between, + /// e.g. parent processes and task hosts of different bitnesses. + /// + private void SetTaskHostEnvironment(Dictionary environment) + { + ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + Dictionary? updatedEnvironment = null; + + if (_updateEnvironment) + { + foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues) + { + string oldValue = variable.Value.Key; + string? newValue = variable.Value.Value; + + // We don't check the return value, because having the variable not exist == be + // null is perfectly valid, and mismatchedEnvironmentValues stores those values + // as null as well, so the String.Equals should still return that they are equal. + environment.TryGetValue(variable.Key, out string? environmentValue); + + if (string.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase)) + { + if (updatedEnvironment == null) + { + if (_updateEnvironmentAndLog) + { + LogMessageFromResource(MessageImportance.Low, SR.ModifyingTaskHostEnvironmentHeader); + } + + updatedEnvironment = new Dictionary(environment, StringComparer.OrdinalIgnoreCase); + } + + if (newValue != null) + { + if (_updateEnvironmentAndLog) + { + LogMessageFromResource(MessageImportance.Low, string.Format(SR.ModifyingTaskHostEnvironmentVariable, variable.Key, newValue, environmentValue ?? string.Empty)); + } + + updatedEnvironment[variable.Key] = newValue; + } + else + { + updatedEnvironment.Remove(variable.Key); + } + } + } + } + + // if it's still null here, there were no changes necessary -- so just + // set it to what was already passed in. + updatedEnvironment ??= environment; + + CommunicationsUtilities.SetEnvironment(updatedEnvironment); + } + + /// + /// Given the environment of the task host at the end of task execution, make sure that any + /// processor-specific variables have been re-applied in the correct form for the main node, + /// so that when we pass this dictionary back to the main node, all it should have to do + /// is just set it. + /// + private Dictionary UpdateEnvironmentForMainNode(Dictionary environment) + { + ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + Dictionary? updatedEnvironment = null; + + if (_updateEnvironment) + { + foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues) + { + // Since this is munging the property list for returning to the parent process, + // then the value we wish to replace is the one that is in this process, and the + // replacement value is the one that originally came from the parent process, + // instead of the other way around. + string? oldValue = variable.Value.Value; + string newValue = variable.Value.Key; + + // We don't check the return value, because having the variable not exist == be + // null is perfectly valid, and mismatchedEnvironmentValues stores those values + // as null as well, so the String.Equals should still return that they are equal. + environment.TryGetValue(variable.Key, out string? environmentValue); + + if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase)) + { + updatedEnvironment ??= new Dictionary(environment, StringComparer.OrdinalIgnoreCase); + + if (newValue != null) + { + updatedEnvironment[variable.Key] = newValue; + } + else + { + updatedEnvironment.Remove(variable.Key); + } + } + } + } + + // if it's still null here, there were no changes necessary -- so just + // set it to what was already passed in. + updatedEnvironment ??= environment; + + return updatedEnvironment; + } + + /// + /// Make sure the mismatchedEnvironmentValues table has been populated. Note that this should + /// only do actual work on the very first run of a task in the task host -- otherwise, it should + /// already have been populated. + /// + private void InitializeMismatchedEnvironmentTable(Dictionary environment) + { + if (s_mismatchedEnvironmentValues == null) + { + // This is the first time that we have received a TaskHostConfiguration packet, so we + // need to construct the mismatched environment table based on our current environment + // (assumed to be effectively identical to startup) and the environment we were given + // via the task host configuration, assumed to be effectively identical to the startup + // environment of the task host, given that the configuration packet is sent immediately + // after the node is launched. + s_mismatchedEnvironmentValues = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair variable in _savedEnvironment!) + { + string? oldValue = variable.Value; + if (!environment.TryGetValue(variable.Key, out string? newValue)) + { + s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(null!, oldValue); + } + else if (!string.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase)) + { + s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue!, oldValue); + } + } + + foreach (KeyValuePair variable in environment) + { + string? newValue = variable.Value; + if (!_savedEnvironment.TryGetValue(variable.Key, out string? oldValue)) + { + s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue!, null); + } + else if (!string.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase)) + { + s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue!, oldValue); + } + } + } + } + + /// + /// Sends the requested packet across to the main node. + /// + private void SendBuildEvent(BuildEventArgs e) + { + if (_nodeEndpoint?.LinkStatus == LinkStatus.Active) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, $"{nameof(_currentConfiguration)} is null."); + + // Types which are not serializable and are not IExtendedBuildEventArgs as + // those always implement custom serialization by WriteToStream and CreateFromStream. + if (!e.GetType().IsSerializable) + { + // log a warning and bail. This will end up re-calling SendBuildEvent, but we know for a fact + // that the warning that we constructed is serializable, so everything should be good. + LogWarningFromResource(string.Format(SR.ExpectedEventToBeSerializable, e.GetType().Name)); + return; + } + + LogMessagePacketBase logMessage = new(new KeyValuePair(_currentConfiguration.NodeId, e)); + _nodeEndpoint.SendData(logMessage); + } + } + + /// + /// Generates the message event corresponding to a particular resource string and set of args. + /// + private void LogMessageFromResource(MessageImportance importance, string message) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log messages!"); + + BuildMessageEventArgs messageArgs = new( + message, + helpKeyword: null, + _currentConfiguration.TaskName, + importance); + + LogMessageEvent(messageArgs); + } + + /// + /// Generates the error event corresponding to a particular resource string and set of args. + /// + private void LogWarningFromResource(string message) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log warnings!"); + + BuildWarningEventArgs warningArgs = new( + subcategory: null, + code: null, + file: ProjectFileOfTaskNode, + lineNumber: LineNumberOfTaskNode, + columnNumber: ColumnNumberOfTaskNode, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: _currentConfiguration.TaskName); + + LogWarningEvent(warningArgs); + } + + /// + /// Generates the error event corresponding to a particular resource string and set of args. + /// + private void LogErrorFromResource(string message) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log errors!"); + + BuildErrorEventArgs errorArgs = new( + subcategory: null, + code: null, + file: ProjectFileOfTaskNode, + lineNumber: LineNumberOfTaskNode, + columnNumber: ColumnNumberOfTaskNode, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: _currentConfiguration.TaskName); + + LogErrorEvent(errorArgs); + } +} diff --git a/src/MSBuildTaskHost/OutOfProcTaskHostTaskResult.cs b/src/MSBuildTaskHost/OutOfProcTaskHostTaskResult.cs new file mode 100644 index 00000000000..f536fab7f99 --- /dev/null +++ b/src/MSBuildTaskHost/OutOfProcTaskHostTaskResult.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Build.TaskHost.BackEnd; + +namespace Microsoft.Build.TaskHost; + +/// +/// A result of executing a target or task. +/// +internal class OutOfProcTaskHostTaskResult +{ + /// + /// The overall result of the task execution. + /// + public TaskCompleteType Result { get; } + + /// + /// Dictionary of the final values of the task parameters. + /// + public Dictionary? FinalParameterValues { get; } + + /// + /// The exception thrown by the task during initialization or execution, if any. + /// + public Exception? TaskException { get; } + + /// + /// The name of the resource representing the message to be logged along with the + /// above exception. + /// + public string? ExceptionMessage { get; } + + /// + /// The arguments to be used when formatting ExceptionMessage. + /// + public string[]? ExceptionMessageArgs { get; } + + private OutOfProcTaskHostTaskResult( + TaskCompleteType result, + Dictionary? finalParameterValues) + { + Result = result; + FinalParameterValues = finalParameterValues; + } + + private OutOfProcTaskHostTaskResult( + TaskCompleteType result, + Exception? taskException = null, + string? exceptionMessage = null, + string[]? exceptionMessageArgs = null) + { + Result = result; + TaskException = taskException; + ExceptionMessage = exceptionMessage; + ExceptionMessageArgs = exceptionMessageArgs; + } + + public static OutOfProcTaskHostTaskResult Success(Dictionary? finalParameterValues) + => new(TaskCompleteType.Success, finalParameterValues); + + public static OutOfProcTaskHostTaskResult Failure(Dictionary? finalParameterValues = null) + => new(TaskCompleteType.Failure, finalParameterValues); + + public static OutOfProcTaskHostTaskResult CrashedAfterExecution(Exception e) + => new(TaskCompleteType.CrashedAfterExecution, e); + + public static OutOfProcTaskHostTaskResult CrashedDuringExecution(Exception e) + => new(TaskCompleteType.CrashedDuringExecution, e); + + public static OutOfProcTaskHostTaskResult CrashedDuringInitialization(Exception e, string resourceId, string[] resourceArgs) + => new(TaskCompleteType.CrashedDuringInitialization, e, resourceId, resourceArgs); +} diff --git a/src/MSBuildTaskHost/Polyfills/CallerArgumentExpressionAttribute.cs b/src/MSBuildTaskHost/Polyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000000..91623fbd9f9 --- /dev/null +++ b/src/MSBuildTaskHost/Polyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_0_OR_GREATER + +using System.Runtime.CompilerServices; + +// This is a supporting forwarder for an internal polyfill API +[assembly: TypeForwardedTo(typeof(CallerArgumentExpressionAttribute))] + +#else + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } +} + +#endif diff --git a/src/MSBuildTaskHost/Polyfills/IsExternalInit.cs b/src/MSBuildTaskHost/Polyfills/IsExternalInit.cs new file mode 100644 index 00000000000..f0a0588d1df --- /dev/null +++ b/src/MSBuildTaskHost/Polyfills/IsExternalInit.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET +using System.Runtime.CompilerServices; + +// Type-forward to the inbox class where available, in order to maintain binary compatibility +// between the .NET and .NET Standard 2.0 assemblies. +[assembly: TypeForwardedTo(typeof(IsExternalInit))] + +#else + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + // Needed so we can use init setters in full fw or netstandard + // (details: https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809) + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} +#endif diff --git a/src/MSBuildTaskHost/NullableAttributes.cs b/src/MSBuildTaskHost/Polyfills/NullableAttributes.cs similarity index 100% rename from src/MSBuildTaskHost/NullableAttributes.cs rename to src/MSBuildTaskHost/Polyfills/NullableAttributes.cs diff --git a/src/MSBuildTaskHost/Properties/AssemblyInfo.cs b/src/MSBuildTaskHost/Properties/AssemblyInfo.cs index 2e74dfc6607..d975ac97636 100644 --- a/src/MSBuildTaskHost/Properties/AssemblyInfo.cs +++ b/src/MSBuildTaskHost/Properties/AssemblyInfo.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM @@ -9,3 +10,5 @@ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] + +[assembly: InternalsVisibleTo("Microsoft.Build.Engine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] diff --git a/src/MSBuildTaskHost/Resources/SR.resx b/src/MSBuildTaskHost/Resources/SR.resx new file mode 100644 index 00000000000..a602e1ccfa1 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/SR.resx @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + + + Parameter "{0}" cannot have zero length. + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.cs.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.cs.xlf new file mode 100644 index 00000000000..bb7a286e7bb --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.cs.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: Hostitel úlohy nástroje MSBuild nepodporuje spouštění úloh, které provádějí zpětná volání rozhraní IBuildEngine. Pokud chcete tyto operace provádět, spusťte svou úlohu v hlavním procesu nástroje MSBuild. Úloha bude automaticky provedena v hostiteli úlohy, pokud v deklaraci UsingTask byly použity atributy s hodnotami Runtime nebo Architecture nebo pokud pro volání úlohy byly použity atributy s hodnotami MSBuildRuntime nebo MSBuildArchitecture, které neodpovídají aktuálnímu modulu runtime nebo architektuře nástroje MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: Bylo nalezeno konfliktní sestavení pro sestavení úlohy {0} v umístění {1}. + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Očekávalo se, že typ události {0} bude možné serializovat pomocí serializátoru .NET. Událost nebylo možné serializovat a byla ignorována. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Předtím, než bude pro hostitele úlohy použito prostředí přijaté z nadřazeného uzlu, budou provedeny jeho následující úpravy: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Nastavení proměnné {0} na hodnotu {1} místo hodnoty {2} proměnné prostředí nadřazeného uzlu + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Cesta: {0} překračuje maximální limit pro cestu k OS. Plně kvalifikovaný název souboru musí být kratší než {1} znaků. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" jsou vyhrazená metadata položky a nemohou být změněna nebo smazána. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Metadata položky %({0}) nelze použít na cestu {1}. {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: Úloha {0} byla označena atributem LoadInSeparateAppDomain, není ale odvozena od třídy MarshalByRefObject. Zkontrolujte, jestli je úloha odvozena od třídy MarshalByRefObject nebo AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Parametr {0} nesmí mít hodnotu Null. + + + + Parameter "{0}" cannot have zero length. + Parametr {0} nesmí mít nulovou délku. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.de.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.de.xlf new file mode 100644 index 00000000000..ce1f99f0250 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.de.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: Der MSBuild-Aufgabenhost unterstützt das Ausführen von Aufgaben nicht, die IBuildEngine-Rückrufe tätigen. Führen Sie Ihre Aufgabe stattdessen im MSBuild-Kernprozess aus, wenn Sie diese Vorgänge durchführen möchten. Wenn UsingTask mit einem Runtime- oder Architecture-Wert oder der Aufgabenaufruf mit einem MSBuildRuntime- oder MSBuildArchitecture-Wert attribuiert wurde, der nicht mit der aktuellen Laufzeit oder Architektur von MSBuild übereinstimmt, wird eine Aufgabe automatisch im Aufgabenhost ausgeführt. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: Eine mit der Aufgabenassembly "{0}" in Konflikt stehende Assembly wurde in "{1}" gefunden. + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Es wurde erwartet, dass der Ereignistyp "{0}" mithilfe des .NET-Serialisierers serialisierbar ist. Das Ereignis war nicht serialisierbar und wurde ignoriert. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Es werden folgende vom übergeordneten Knoten empfangene Änderungen an der Umgebung vorgenommen, bevor sie auf den Aufgabenhost angewendet wird: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Festlegen von "{0}" auf "{1}" statt auf den Wert "{2}" der übergeordneten Umgebung. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Der Pfad "{0}" überschreitet das maximale Pfadlimit des Betriebssystems. Der vollqualifizierte Dateiname muss weniger als {1} Zeichen umfassen. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" sind reservierte Elementmetadaten, die nicht geändert oder gelöscht werden können. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Die %({0})-Elementmetadaten können nicht auf den Pfad "{1}" angewendet werden. {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: Die Aufgabe "{0}" wurde mit dem LoadInSeparateAppDomain-Attribut markiert, ist jedoch nicht von MarshalByRefObject abgeleitet. Stellen Sie sicher, dass die Aufgabe von MarshalByRefObject oder AppDomainIsolatedTask abgeleitet wird. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Der Parameter "{0}" darf nicht NULL sein. + + + + Parameter "{0}" cannot have zero length. + Parameter "{0}" darf nicht die Länge NULL haben. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.es.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.es.xlf new file mode 100644 index 00000000000..9b8aa0f86df --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.es.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: El host de tareas de MSBuild no admite la ejecución de tareas que realizan devoluciones de llamadas de IBuildEngine. Si desea realizar estas operaciones, ejecute la tarea en el proceso de MSBuild principal en su lugar. Se ejecutará una tarea automáticamente en el host de tareas si a UsingTask se le ha agregado un atributo con un valor "Runtime" o "Arquitectura", o bien a la invocación de tareas se le ha agregado un atributo con un valor "MSBuildRuntime" o "MSBuildArchitecture", que no coincide con el runtime o la arquitectura actual de MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: Se detectó un ensamblado conflictivo para el ensamblado de tarea "{0}" en "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Se esperaba que el tipo de evento "{0}" fuera serializable con el serializador .NET. El evento no era serializable y se ha omitido. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Se están realizando las siguientes modificaciones en el entorno recibido del nodo primario antes de aplicarlo al host de tareas: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Estableciendo '{0}' en '{1}' en lugar del valor del entorno primario, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + La ruta de acceso {0} supera el límite máximo para la ruta de acceso del sistema operativo. El nombre de archivo completo debe ser inferior a {1} caracteres. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" es un metadato de elemento reservado y no se puede modificar ni eliminar. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Los metadatos "%({0})" del elemento no pueden aplicarse a la ruta de acceso "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: La tarea "{0}" se marcó con el atributo LoadInSeparateAppDomain, pero esta no deriva de MarshalByRefObject. Asegúrese de que la tarea deriva de MarshalByRefObject o de AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + El parámetro "{0}" no puede ser NULL. + + + + Parameter "{0}" cannot have zero length. + La longitud del parámetro "{0}" no puede ser cero. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.fr.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.fr.xlf new file mode 100644 index 00000000000..aa78715eb49 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.fr.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: L'hôte de tâche MSBuild ne prend pas en charge l'exécution de tâches qui effectuent des rappels IBuildEngine. Pour effectuer ces opérations, exécutez plutôt votre tâche dans le processus MSBuild de base. Une tâche s'exécute automatiquement dans l'hôte de tâche si UsingTask a été défini sur la valeur "Runtime" ou "Architecture", ou si l'appel de la tâche a été défini sur la valeur "MSBuildRuntime" ou "MSBuildArchitecture", qui ne correspond pas à l'architecture ou au runtime actuel de MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: un assembly en conflit avec l'assembly de tâche "{0}" a été trouvé sur "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Le type d'événement "{0}" devait être sérialisable à l'aide du sérialiseur .NET. L'événement n'était pas sérialisable et a été ignoré. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Modifications suivantes en cours sur l'environnement reçu du nœud parent avant son application à l'hôte de tâche : + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Définition de '{0}' sur '{1}' plutôt que sur la valeur de l'environnement parent, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Le chemin {0} dépasse la limite maximale de chemin du système d'exploitation. Le nom du fichier qualifié complet doit contenir moins de {1} caractères. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" est une métadonnée d'élément réservée qui ne peut être ni modifiée ni supprimée. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Impossible d'appliquer la métadonnée d'élément "%({0})" au chemin d'accès "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: la tâche "{0}" a été marquée avec l'attribut LoadInSeparateAppDomain, mais ne dérive pas de MarshalByRefObject. Vérifiez que la tâche dérive de MarshalByRefObject ou de AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Le paramètre "{0}" ne peut pas être null. + + + + Parameter "{0}" cannot have zero length. + La longueur du paramètre "{0}" ne peut pas être égale à zéro. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.it.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.it.xlf new file mode 100644 index 00000000000..a4cee485754 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.it.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: l'host attività MSBuild non supporta l'esecuzione di attività che eseguono callback IBuildEngine. Per eseguire tali operazioni, eseguire l'attività nel processo MSBuild di base. Un'attività verrà automaticamente eseguita nell'host attività se per UsingTask è stato definito un attributo con valore "Runtime" o "Architecture" o se per la chiamata all'attività è stato definito un attributo con valore "MSBuildRuntime" o "MSBuildArchitecture" che non corrisponde al runtime corrente o all'architettura di MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: rilevato un assembly in conflitto per l'assembly dell'attività "{0}" in "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + È previsto un tipo di evento "{0}" serializzabile con il serializzatore .NET. L'evento non era serializzabile ed è stato ignorato. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Le modifiche seguenti verranno apportate all'ambiente ricevuto dal nodo padre prima dell'applicazione all'host attività: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Impostazione di '{0}' su '{1}' anziché sul valore dell'ambiente padre, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Il percorso {0} supera il limite massimo dei percorsi del sistema operativo. Il nome completo del file deve essere composto da meno di {1}. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" è un metadato di un elemento riservato e pertanto non può essere modificato o eliminato. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Non è possibile applicare i metadati dell'elemento "%({0})" al percorso "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: l'attività "{0}" è stata contrassegnata con l'attributo LoadInSeparateAppDomain, ma non deriva da MarshalByRefObject. Verificare che l'attività derivi da MarshalByRefObject o AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Il parametro "{0}" non può essere Null. + + + + Parameter "{0}" cannot have zero length. + Il parametro "{0}" non può avere lunghezza zero. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.ja.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.ja.xlf new file mode 100644 index 00000000000..9f282723a81 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.ja.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: MSBuild タスク ホストは、IBuildEngine コールバックを実行するタスクの実行をサポートしていません。これらの操作を実行する場合は、タスクをコア MSBuild プロセスで実行してください。UsingTask の属性として設定されている "Runtime" または "Architecture" の値、あるいはタスク呼び出しの属性として設定されている "MSBuildRuntime" または "MSBuildArchitecture" の値が MSBuild の現在のランタイムまたはアーキテクチャと一致しない場合、タスクは自動的にタスク ホストで実行されます。 + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: タスク アセンブリ "{0}" に対して競合しているアセンブリが "{1}" で見つかりました。 + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + イベントの種類 "{0}" は .NET シリアライザーを使用してシリアル化可能であることが想定されていましたが、シリアル化可能でなかったため無視されました。 + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + 親ノードから受け取った環境をタスク ホストに適用する前に、次の変更を行っています: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + '{0}' を親環境の値 '{2}' ではなく '{1}' に設定しています。 + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + パス: {0} は OS のパスの上限を越えています。完全修飾のファイル名は {1} 文字以下にする必要があります。 + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" は予約された項目メタデータです。変更または削除することはできません。 + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + 項目メタデータ "%({0})" をパス "{1}" に適用できません。{2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: "{0}" タスクに属性 LoadInSeparateAppDomain が設定されていますが、MarshalByRefObject から派生していません。そのタスクが MarshalByRefObject または AppDomainIsolatedTask から派生していることを確認してください。 + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + パラメーター "{0}" を null にすることはできません。 + + + + Parameter "{0}" cannot have zero length. + パラメーター "{0}" の長さを 0 にすることはできません。 + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.ko.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.ko.xlf new file mode 100644 index 00000000000..8277e15c84b --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.ko.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: MSBuild 작업 호스트는 IBuildEngine 콜백을 수행하는 작업의 실행을 지원하지 않습니다. 이러한 작업을 수행하려면 대신 코어 MSBuild 프로세스에서 작업을 실행하세요. UsingTask에 "Runtime" 또는 "Architecture" 값이 사용되었거나 작업 호출에 MSBuild의 현재 런타임 또는 아키텍처와 일치하지 않는 "MSBuildRuntime" 또는 "MSBuildArchitecture" 값이 사용된 경우 작업 호스트에서 작업이 자동으로 실행됩니다. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: 작업 어셈블리 "{0}"과(와) 충돌하는 어셈블리가 "{1}"에 있습니다. + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + 이벤트 유형 "{0}"은(는) .NET serializer를 사용하여 serialize할 수 있어야 합니다. 이 이벤트는 serialize할 수 없으므로 무시되었습니다. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + 작업 호스트에 적용하기 전에 부모 노드로부터 받은 환경을 다음과 같이 수정하고 있습니다. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + '{0}'을(를) 부모 환경 값인 '{2}' 대신 '{1}'(으)로 설정하고 있습니다. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + 경로: {0}은(는) OS 최대 경로 제한을 초과합니다. 정규화된 파일 이름은 {1}자 이하여야 합니다. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}"은(는) 예약된 항목 메타데이터이므로 수정 또는 삭제할 수 없습니다. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + 항목 메타데이터 "%({0})"을(를) "{1}" 경로에 적용할 수 없습니다. {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: "{0}" 작업이 LoadInSeparateAppDomain 특성으로 표시되었지만 MarshalByRefObject에서 파생되지 않습니다. 해당 작업이 MarshalByRefObject 또는 AppDomainIsolatedTask에서 파생되는지 확인하십시오. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + "{0}" 매개 변수는 null일 수 없습니다. + + + + Parameter "{0}" cannot have zero length. + "{0}" 매개 변수의 길이는 0일 수 없습니다. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.pl.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.pl.xlf new file mode 100644 index 00000000000..e6137d07284 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.pl.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: Host zadań programu MSBuild nie obsługuje zadań wykonujących wywołania zwrotne do aparatu IBuildEngine. Jeśli chcesz wykonywać te operacje, uruchom zadanie w podstawowym procesie programu MSBuild. Zadanie będzie automatycznie wykonywane na hoście zadań, jeśli w deklaracji UsingTask ustawiono wartość „Runtime” lub „Architecture” albo w wywołaniu zadania ustawiono wartość „MSBuildRuntime” lub „MSBuildArchitecture”, która nie odpowiada bieżącemu środowisku uruchomieniowemu lub architekturze programu MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: Zestaw, który wywołuje konflikt z zestawem zadania „{0}”, został znaleziony w „{1}”. + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Oczekiwano, że zdarzenie typu „{0}” będzie uszeregowane przy użyciu serializatora platformy .NET. Zdarzenie nie może podlegać szeregowaniu, dlatego zostało zignorowane. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Wymienione zmiany otrzymane z węzła nadrzędnego zostaną wprowadzone w środowisku, a po sprawdzeniu działania zastosowane do hosta zadań: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Ustawienie dla elementu „{0}” wartości „{1}” zamiast wartości „{2}” obowiązującej w środowisku nadrzędnym. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Ścieżka: {0} przekracza limit maksymalnej długości ścieżki w systemie operacyjnym. W pełni kwalifikowana nazwa pliku musi się składać z mniej niż {1} znaków. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + „{0}” jest zastrzeżonym elementem metadanych i nie może zostać zmodyfikowany ani usunięty. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Elementu metadanych „%({0})” nie można zastosować do ścieżki „{1}”. {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: Zadanie „{0}” zostało oznaczone atrybutem LoadInSeparateAppDomain, ale nie pochodzi od obiektu MarshalByRefObject. Sprawdź, czy zadanie pochodzi od obiektu MarshalByRefObject lub zadania AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Parametr „{0}” nie może być zerowy. + + + + Parameter "{0}" cannot have zero length. + Parametr „{0}” nie może mieć zerowej długości. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.pt-BR.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.pt-BR.xlf new file mode 100644 index 00000000000..727f4447193 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.pt-BR.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: O host de tarefas MSBuild não dá suporte a tarefas em execução que executam chamadas de retorno de IBuildEngine. Se desejar executar essas operações, execute sua tarefa no processo MSBuild principal. Uma tarefa será automaticamente executada no host de tarefas se UsingTask tiver como atributo um valor "Runtime" ou "Architecture" ou se a invocação da tarefa tiver como atributo um valor "MSBuildRuntime" ou "MSBuildArchitecture", que não coincida com o tempo de execução ou arquitetura atual do MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: Foi encontrado um assembly conflitante no assembly da tarefa "{0}" em "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Era esperado que o tipo de evento "{0}" fosse serializável usando o serializador .NET. O evento não era serializável e foi ignorado. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Fazendo as seguintes modificações no ambiente recebido do nó pai antes de aplicá-lo ao host de tarefas: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Definindo "{0}" como "{1}" em vez do valor do ambiente pai, "{2}". + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Caminho: {0} excede o limite máximo do caminho do SO. O nome do arquivo totalmente qualificado deve ter menos de {1} caracteres. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" são metadados de item reservado e não podem ser modificados ou excluídos. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Os metadados do item "%({0})" não podem ser aplicados ao caminho "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: A tarefa "{0}" foi marcada com o atributo LoadInSeparateAppDomain, mas não é derivada de MarshalByRefObject. Verifique se a tarefa é derivada de MarshalByRefObject ou de AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + O parâmetro "{0}" não pode ser nulo. + + + + Parameter "{0}" cannot have zero length. + O parâmetro "{0}" não pode ter comprimento zero. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.ru.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.ru.xlf new file mode 100644 index 00000000000..b405bcbdb3f --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.ru.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: узел сборки MSBuild не поддерживает задачи, которые выполняют обратные вызовы IBuildEngine. Если необходимо выполнить эти операции, запустите задачу в основном процессе MSBuild. Задача будет автоматически выполнена в узле задач, если для UsingTask был указан атрибут со значением "Runtime" или "Architecture" или для вызова задачи был указан атрибут со значением "MSBuildRuntime" или "MSBuildArchitecture", которые не соответствуют текущей среде выполнения или архитектуре MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: в "{1}" обнаружена сборка, конфликтующая со сборкой задачи "{0}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + Необходимо, чтобы тип события "{0}" был сериализуемым с помощью сериализатора .NET. Событие не было сериализуемым и было пропущено. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Перед применением окружения, полученного от родительского узла, к серверу задач в нем выполняются следующие изменения: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Задание для "{0}" значения "{1}", а не значения родительского окружения "{2}". + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Длина пути {0} превышает максимально допустимую в ОС. Символов в полном имени файла должно быть не больше {1}. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" - зарезервированные метаданные элемента; их изменение или удаление невозможно. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + Не удается применить метаданные элемента "%({0})" к пути "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: задача "{0}" помечена атрибутом LoadInSeparateAppDomain, но не является производной от MarshalByRefObject. Задача должна быть производной от MarshalByRefObject или AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + Параметр "{0}" не может иметь неопределенное (null) значение. + + + + Parameter "{0}" cannot have zero length. + Длина параметра "{0}" не может быть равна нулю. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.tr.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.tr.xlf new file mode 100644 index 00000000000..5628d348936 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.tr.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: MSBuild görev konağı IBuildEngine geri çağırmaları gerçekleştiren görevleri çalıştırmayı desteklemez. Bu işlemleri gerçekleştirmek istiyorsanız lütfen bunun yerine görevinizi çekirdek MSBuild işleminde gerçekleştirin. UsingTask öğesine bir "Runtime" veya "Architecture" değeri atfedilmişse veya görev çağrısına MSBuild’in geçerli çalışma zamanı ya da mimarisi ile eşleşmeyen bir "MSBuildRuntime" veya "MSBuildArchitecture" değeri atfedilmişse görev konağında bir görev otomatik olarak yürütülür. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: "{0}" görev derlemesi için "{1}" konumunda çakışan derleme bulundu. + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + "{0}" olay türünün .NET serileştiricisi kullanılarak serileştirilebilir olması bekleniyordu. Olay serileştirilebilir değildi ve yoksayıldı. + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Üst düğümden alınan ortam görev ana bilgisayarına uygulanmadan önce ortamda aşağıdaki değişiklikler yapılıyor: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + '{0}', üst ortamında değeri olan '{2}' yerine '{1}' değerine ayarlanıyor. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + Yol: {0}, işletim sisteminin en yüksek yol sınırını aşıyor. Tam dosya adı en fazla {1} karakter olmalıdır. + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" ayrılmış bir öğe meta verisi olduğu için değiştirilemez veya silinemez. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + "%({0})" öğe meta verisi "{1}" yoluna uygulanamıyor. {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: "{0}" görevi LoadInSeparateAppDomain özniteliğiyle işaretlenmiş, ancak MarshalByRefObject öğesinden türetilmiyor. Görevin MarshalByRefObject veya AppDomainIsolatedTask öğesinden türetildiğini denetleyin. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + "{0}" parametresi null olamaz. + + + + Parameter "{0}" cannot have zero length. + "{0}" parametresinin uzunluğu sıfır olamaz. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.xlf new file mode 100644 index 00000000000..3d8ccfdc9e1 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.xlf @@ -0,0 +1,235 @@ + + + + + + + MSB4188: Build was canceled. + {StrBegin="MSB4188: "} Error when the build stops suddenly for some reason. For example, because a child node died. + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + Build started. + + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + + + + {0} ({1},{2}) + A file location to be embedded in a string. + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + MSB4025: The project file could not be loaded. {0} + {StrBegin="MSB4025: "}UE: This message is shown when the project file given to the engine cannot be loaded because the filename/path is + invalid, or due to lack of permissions, or incorrect XML. The project filename is not part of the message because it is + provided separately to loggers. + LOCALIZATION: {0} is a localized message from the CLR/FX explaining why the project is invalid. + + + MSB4103: "{0}" is not a valid logger verbosity level. + {StrBegin="MSB4103: "} + + + MSBuild is expecting a valid "{0}" object. + + + + MSB4132: The tools version "{0}" is unrecognized. Available tools versions are {1}. + {StrBegin="MSB4132: "}LOCALIZATION: {1} contains a comma separated list. + + + MSB5016: The name "{0}" contains an invalid character "{1}". + {StrBegin="MSB5016: "} + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The string "{0}" cannot be converted to a boolean (true/false) value. + + + + MSB5003: Failed to create a temporary file. Temporary files folder is full or its path is incorrect. {0} + {StrBegin="MSB5003: "} + + + MSB5018: Failed to delete the temporary file "{0}". {1} + {StrBegin="MSB5018: "} + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + .NET Framework version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion. + + + + .NET Framework version "{0}" is not supported when explicitly targeting the Windows SDK, which is only supported on .NET 4.5 and later. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion that is Version45 or above. + + + + Visual Studio version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.VisualStudioVersion. + + + + When attempting to generate a reference assembly path from the path "{0}" and the framework moniker "{1}" there was an error. {2} + No Error code because this resource will be used in an exception. The error code is discarded if it is included + + + Could not find directory path: {0} + Directory must exist + + + You do not have access to: {0} + Directory must have access + + + Schema validation + + UE: this fragment is used to describe errors that are caused by schema validation. For example, if a normal error is + displayed like this: "MSBUILD : error MSB0000: This is an error.", then an error from schema validation would look like this: + "MSBUILD : Schema validation error MSB0000: This is an error." + LOCALIZATION: This fragment needs to be localized. + + + + MSB5002: The task executable has not completed within the specified limit of {0} milliseconds, terminating. + {StrBegin="MSB5002: "} + + + Parameter "{0}" cannot be null. + + + + Parameter "{0}" cannot have zero length. + + + + Parameters "{0}" and "{1}" must have the same number of elements. + + + + The resource string "{0}" for the "{1}" task cannot be found. Confirm that the resource name "{0}" is correctly spelled, and the resource exists in the task's assembly. + + + + The "{0}" task has not registered its resources. In order to use the "TaskLoggingHelper.FormatResourceString()" method this task needs to register its resources either during construction, or via the "TaskResources" property. + LOCALIZATION: "TaskLoggingHelper.FormatResourceString()" and "TaskResources" should not be localized. + + + MSB5004: The solution file has two projects named "{0}". + {StrBegin="MSB5004: "}UE: The solution filename is provided separately to loggers. + + + MSB5005: Error parsing project section for project "{0}". The project file name "{1}" contains invalid characters. + {StrBegin="MSB5005: "}UE: The solution filename is provided separately to loggers. + + + MSB5006: Error parsing project section for project "{0}". The project file name is empty. + {StrBegin="MSB5006: "}UE: The solution filename is provided separately to loggers. + + + MSB5007: Error parsing the project configuration section in solution file. The entry "{0}" is invalid. + {StrBegin="MSB5007: "}UE: The solution filename is provided separately to loggers. + + + MSB5008: Error parsing the solution configuration section in solution file. The entry "{0}" is invalid. + {StrBegin="MSB5008: "}UE: The solution filename is provided separately to loggers. + + + MSB5009: Error parsing the nested project section in solution file. + {StrBegin="MSB5009: "}UE: The solution filename is provided separately to loggers. + + + MSB5023: Error parsing the nested project section in solution file. A project with the GUID "{0}" is listed as being nested under project "{1}", but does not exist in the solution. + {StrBegin="MSB5023: "}UE: The solution filename is provided separately to loggers. + + + MSB5010: No file format header found. + {StrBegin="MSB5010: "}UE: The solution filename is provided separately to loggers. + + + MSB5011: Parent project GUID not found in "{0}" project dependency section. + {StrBegin="MSB5011: "}UE: The solution filename is provided separately to loggers. + + + MSB5012: Unexpected end-of-file reached inside "{0}" project section. + {StrBegin="MSB5012: "}UE: The solution filename is provided separately to loggers. + + + MSB5013: Error parsing a project section. + {StrBegin="MSB5013: "}UE: The solution filename is provided separately to loggers. + + + MSB5014: File format version is not recognized. MSBuild can only read solution files between versions {0}.0 and {1}.0, inclusive. + {StrBegin="MSB5014: "}UE: The solution filename is provided separately to loggers. + + + MSB5015: The properties could not be read from the WebsiteProperties section of the "{0}" project. + {StrBegin="MSB5015: "}UE: The solution filename is provided separately to loggers. + + + Unrecognized solution version "{0}", attempting to continue. + + + + Solution file + UE: this fragment is used to describe errors found while parsing solution files. For example, if a normal error is + displayed like this: "MSBUILD : error MSB0000: This is an error.", then an error from solution parsing would look like this: + "MSBUILD : Solution file error MSB0000: This is an error." + LOCALIZATION: This fragment needs to be localized. + + + MSB5019: The project file is malformed: "{0}". {1} + {StrBegin="MSB5019: "} + + + MSB5020: Could not load the project file: "{0}". {1} + {StrBegin="MSB5020: "} + + + MSB5021: "{0}" and its child processes are being terminated in order to cancel the build. + {StrBegin="MSB5021: "} + + + This collection is read-only. + + + + MSB5024: Could not determine a valid location to MSBuild. Try running this process from the Developer Command Prompt for Visual Studio. + {StrBegin="MSB5021: "} + + + MSB4233: There was an exception while reading the log file: {0} + {StrBegin="MSB4233: "}This is shown when the Binary Logger can't read the log file. + + + Parameter "{0}" with assigned value "{1}" cannot have invalid path or invalid file characters. + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hans.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hans.xlf new file mode 100644 index 00000000000..2c1ed366f5b --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hans.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: MSBuild 任务宿主不支持运行执行 IBuildEngine 回调的任务。如果需要执行这些操作,请改为在核心 MSBuild 进程中运行您的任务。如果 UsingTask 已用“Runtime”或“Architecture”值特性化,或者任务调用已用“MSBuildRuntime”或“MSBuildArchitecture”值(与 MSBuild 的当前运行时或体系结构不匹配)特性化,则将自动在任务宿主中执行任务。 + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: 在“{1}”处发现了与任务程序集“{0}”冲突的程序集。 + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + 事件类型“{0}”应可以使用 .NET 序列化程序进行序列化。此事件不可序列化,已忽略它。 + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + 先对从父节点收到的环境进行以下修改,然后再将其应用于任务宿主: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + 将“{0}”设置为“{1}”,而不是父环境的值“{2}”。 + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + 路径: {0} 超过 OS 最大路径限制。完全限定的文件名必须少于 {1} 个字符。 + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + “{0}”是保留的项元数据,不能修改或删除。 + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + 无法将项元数据“%({0})”应用于路径“{1}”。{2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: “{0}”任务已标记为 LoadInSeparateAppDomain 特性,但未派生自 MarshalByRefObject。请检查该任务是派生自 MarshalByRefObject 还是 AppDomainIsolatedTask。 + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + 参数“{0}”不能为 null。 + + + + Parameter "{0}" cannot have zero length. + 参数“{0}”的长度不能为零。 + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hant.xlf b/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hant.xlf new file mode 100644 index 00000000000..43f6f0bb8b0 --- /dev/null +++ b/src/MSBuildTaskHost/Resources/xlf/SR.zh-Hant.xlf @@ -0,0 +1,62 @@ + + + + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + MSB5022: MSBuild 工作主機不支援會執行 IBuildEngine 回撥的工作。若您想要執行這些作業,請改為在核心 MSBuild 處理序執行您的工作。若 UsingTask 已由 "Runtime" 或 "Architecture" 值賦予屬性,或工作引動過程已由 "MSBuildRuntime" 或 "MSBuildArchitecture" 值賦予屬性 (不符合 MSBuild 的目前執行階段或結構),則工作將自動在工作主機中執行。 + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + MSB4008: 已在 "{1}" 中發現工作組件 "{0}" 的衝突組件。 + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + 事件類型 "{0}" 應該可以使用 .NET 序列化程式序列化。此事件不可序列化,已略過。 + + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + 在套用到工作主機之前,對從父節點接收的環境進行下列修改: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + 將 '{0}' 設定為 '{1}' 而非父環境的值 '{2}。 + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Path: {0} exceeds the OS max path limit. The fully qualified file name must be less than {1} characters. + 路徑: {0} 超過 OS 路徑上限。完整檔案名稱必須少於 {1} 個字元。 + + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + "{0}" 是保留的項目中繼資料,不能修改或刪除。 + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + 無法將項目中繼資料 "%({0})" 套用至路徑 "{1}"。{2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + MSB4077: "{0}" 工作已經以屬性 LoadInSeparateAppDomain 標記,但不是衍生自 MarshalByRefObject。請檢查工作是否衍生自 MarshalByRefObject 或 AppDomainIsolatedTask。 + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + Parameter "{0}" cannot be null. + 參數 "{0}" 不能為 null。 + + + + Parameter "{0}" cannot have zero length. + 參數 "{0}" 長度不能為零。 + + + + + \ No newline at end of file diff --git a/src/MSBuildTaskHost/TaskLoader.cs b/src/MSBuildTaskHost/TaskLoader.cs new file mode 100644 index 00000000000..bea7bfb123c --- /dev/null +++ b/src/MSBuildTaskHost/TaskLoader.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.Resources; +using Microsoft.Build.TaskHost.Utilities; + +namespace Microsoft.Build.TaskHost; + +/// +/// Class for loading tasks. +/// +internal static class TaskLoader +{ + /// + /// For saving the assembly that was loaded by the TypeLoader + /// We only use this when the assembly failed to load properly into the appdomain. + /// + private static LoadedType? s_resolverLoadedType; + + /// + /// Delegate for logging task loading errors. + /// + internal delegate void LogError(string taskLocation, int taskLine, int taskColumn, string message); + + /// + /// Checks if the given type is a task factory. + /// + /// This method is used as a type filter delegate. + /// true, if specified type is a task + internal static bool IsTaskClass(Type type, object unused) + => type.IsClass && !type.IsAbstract && type.GetInterface("Microsoft.Build.Framework.ITask") != null; + + /// + /// Creates an ITask instance and returns it. + /// + internal static ITask? CreateTask( + LoadedType loadedType, + string taskName, + string taskLocation, + int taskLine, + int taskColumn, + LogError logError, + AppDomainSetup appDomainSetup, + Action? appDomainCreated, + bool isOutOfProc, + out AppDomain? taskAppDomain) + { + bool separateAppDomain = loadedType.HasLoadInSeparateAppDomainAttribute; + s_resolverLoadedType = null; + taskAppDomain = null; + ITask? taskInstanceInOtherAppDomain = null; + + try + { + if (separateAppDomain) + { + if (!loadedType.IsMarshalByRef) + { + logError(taskLocation, taskLine, taskColumn, string.Format(SR.TaskNotMarshalByRef, taskName)); + return null; + } + else + { + // Our task depend on this name to be precisely that, so if you change it make sure + // you also change the checks in the tasks run in separate AppDomains. Better yet, just don't change it. + + // Make sure we copy the appdomain configuration and send it to the appdomain we create so that if the creator of the current appdomain + // has done the binding redirection in code, that we will get those settings as well. + AppDomainSetup appDomainInfo = new AppDomainSetup(); + + // Get the current app domain setup settings + byte[] currentAppdomainBytes = appDomainSetup.GetConfigurationBytes(); + + // Apply the appdomain settings to the new appdomain before creating it + appDomainInfo.SetConfigurationBytes(currentAppdomainBytes); + + AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver; + s_resolverLoadedType = loadedType; + + taskAppDomain = AppDomain.CreateDomain(isOutOfProc ? "taskAppDomain (out-of-proc)" : "taskAppDomain (in-proc)", null, appDomainInfo); + + if (loadedType.LoadedAssembly != null) + { + taskAppDomain.Load(loadedType.LoadedAssemblyName); + } + + // Hook up last minute dumping of any exceptions + taskAppDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler; + appDomainCreated?.Invoke(taskAppDomain); + } + } + else + { + // perf improvement for the same appdomain case - we already have the type object + // and don't want to go through reflection to recreate it from the name. + return (ITask?)Activator.CreateInstance(loadedType.Type); + } + + if (loadedType.AssemblyFilePath != null) + { + taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceFromAndUnwrap(loadedType.AssemblyFilePath, loadedType.Type.FullName); + + // this will force evaluation of the task class type and try to load the task assembly + Type taskType = taskInstanceInOtherAppDomain.GetType(); + + // If the types don't match, we have a problem. It means that our AppDomain was able to load + // a task assembly using Load, and loaded a different one. I don't see any other choice than + // to fail here. + if (taskType != loadedType.Type) + { + logError(taskLocation, taskLine, taskColumn, string.Format(SR.ConflictingTaskAssembly, loadedType.AssemblyFilePath, loadedType.Type.Assembly.Location)); + taskInstanceInOtherAppDomain = null; + } + } + else + { + taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); + } + + return taskInstanceInOtherAppDomain; + } + finally + { + // Don't leave appdomains open + if (taskAppDomain != null && taskInstanceInOtherAppDomain == null) + { + AppDomain.Unload(taskAppDomain); + RemoveAssemblyResolver(); + } + } + } + + /// + /// This is a resolver to help created AppDomains when they are unable to load an assembly into their domain we will help + /// them succeed by providing the already loaded one in the currentdomain so that they can derive AssemblyName info from it + /// + private static Assembly? AssemblyResolver(object sender, ResolveEventArgs args) + { + if (args.Name.Equals(s_resolverLoadedType?.LoadedAssemblyName.FullName, StringComparison.OrdinalIgnoreCase)) + { + if (s_resolverLoadedType == null || s_resolverLoadedType.Path == null) + { + return null; + } + + return s_resolverLoadedType.LoadedAssembly ?? Assembly.Load(s_resolverLoadedType.Path); + } + + return null; + } + + /// + /// Check if we added a resolver and remove it + /// + internal static void RemoveAssemblyResolver() + { + if (s_resolverLoadedType != null) + { + AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolver; + s_resolverLoadedType = null; + } + } +} diff --git a/src/MSBuildTaskHost/Traits.cs b/src/MSBuildTaskHost/Traits.cs new file mode 100644 index 00000000000..0cd33e79dde --- /dev/null +++ b/src/MSBuildTaskHost/Traits.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.TaskHost; + +/// +/// Represents toggleable features of the MSBuild engine. +/// +internal sealed class Traits +{ + public static Traits Instance { get; } = new Traits(); + + private Traits() + { + EscapeHatches = new EscapeHatches(); + + DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); + DebugNodeCommunication = DebugEngine || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM")); + } + + public EscapeHatches EscapeHatches { get; } + + public readonly bool DebugEngine; + public readonly bool DebugNodeCommunication; +} + +internal sealed class EscapeHatches +{ + /// + /// Allow node reuse of TaskHost nodes. This results in task assemblies locked past the build lifetime, preventing them from being rebuilt if custom tasks change, but may improve performance. + /// + public readonly bool ReuseTaskHostNodes = Environment.GetEnvironmentVariable("MSBUILDREUSETASKHOSTNODES") == "1"; + + /// + /// Disable the use of paths longer than Windows MAX_PATH limits (260 characters) when running on a long path enabled OS. + /// + public readonly bool DisableLongPaths = Environment.GetEnvironmentVariable("MSBUILDDISABLELONGPATHS") == "1"; +} diff --git a/src/MSBuildTaskHost/TypeLoader.cs b/src/MSBuildTaskHost/TypeLoader.cs index 741ec5db9fa..9e8eeb598f8 100644 --- a/src/MSBuildTaskHost/TypeLoader.cs +++ b/src/MSBuildTaskHost/TypeLoader.cs @@ -6,329 +6,268 @@ using System.IO; using System.Reflection; using System.Threading; -using Microsoft.Build.Framework; +using Microsoft.Build.TaskHost.Collections; +using Microsoft.Build.TaskHost.Utilities; -#nullable disable +namespace Microsoft.Build.TaskHost; -namespace Microsoft.Build.Shared +/// +/// This class is used to load types from their assemblies. +/// +internal sealed class TypeLoader { /// - /// This class is used to load types from their assemblies. + /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter. /// - internal class TypeLoader - { - /// - /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter. - /// - private static Concurrent.ConcurrentDictionary> s_cacheOfLoadedTypesByFilter = new Concurrent.ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> s_cacheOfLoadedTypesByFilter = new(); - /// - /// Cache to keep track of the assemblyLoadInfos based on a given type filter for assemblies which are to be loaded for reflectionOnlyLoads. - /// - private static Concurrent.ConcurrentDictionary> s_cacheOfReflectionOnlyLoadedTypesByFilter = new Concurrent.ConcurrentDictionary>(); + /// + /// Typefilter for this typeloader + /// + private readonly TypeFilter _isDesiredType; - /// - /// Typefilter for this typeloader - /// - private TypeFilter _isDesiredType; + /// + /// Constructor. + /// + internal TypeLoader(TypeFilter isDesiredType) + { + ErrorUtilities.VerifyThrow(isDesiredType != null, "need a type filter"); - /// - /// Constructor. - /// - internal TypeLoader(TypeFilter isDesiredType) - { - ErrorUtilities.VerifyThrow(isDesiredType != null, "need a type filter"); + _isDesiredType = isDesiredType; + } - _isDesiredType = isDesiredType; - } + /// + /// Given two type names, looks for a partial match between them. A partial match is considered valid only if it occurs on + /// the right side (tail end) of the name strings, and at the start of a class or namespace name. + /// + /// + /// 1) Matches are case-insensitive. + /// 2) .NET conventions regarding namespaces and nested classes are respected, including escaping of reserved characters. + /// + /// + /// "Csc" and "csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Microsoft.Build.Tasks.Csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Csc" ==> partial match + /// "Microsoft.Build.Tasks.Csc" and "Tasks.Csc" ==> partial match + /// "MyTasks.ATask+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.ATask\\+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.CscTask" and "Csc" ==> no match + /// "MyTasks.MyCsc" and "Csc" ==> no match + /// "MyTasks.ATask\.Csc" and "Csc" ==> no match + /// "MyTasks.ATask\\\.Csc" and "Csc" ==> no match + /// + /// true, if the type names match exactly or partially; false, if there is no match at all + internal static bool IsPartialTypeNameMatch(string typeName1, string typeName2) + { + bool isPartialMatch = false; - /// - /// Given two type names, looks for a partial match between them. A partial match is considered valid only if it occurs on - /// the right side (tail end) of the name strings, and at the start of a class or namespace name. - /// - /// - /// 1) Matches are case-insensitive. - /// 2) .NET conventions regarding namespaces and nested classes are respected, including escaping of reserved characters. - /// - /// - /// "Csc" and "csc" ==> exact match - /// "Microsoft.Build.Tasks.Csc" and "Microsoft.Build.Tasks.Csc" ==> exact match - /// "Microsoft.Build.Tasks.Csc" and "Csc" ==> partial match - /// "Microsoft.Build.Tasks.Csc" and "Tasks.Csc" ==> partial match - /// "MyTasks.ATask+NestedTask" and "NestedTask" ==> partial match - /// "MyTasks.ATask\\+NestedTask" and "NestedTask" ==> partial match - /// "MyTasks.CscTask" and "Csc" ==> no match - /// "MyTasks.MyCsc" and "Csc" ==> no match - /// "MyTasks.ATask\.Csc" and "Csc" ==> no match - /// "MyTasks.ATask\\\.Csc" and "Csc" ==> no match - /// - /// true, if the type names match exactly or partially; false, if there is no match at all - internal static bool IsPartialTypeNameMatch(string typeName1, string typeName2) + // if the type names are the same length, a partial match is impossible + if (typeName1.Length != typeName2.Length) { - bool isPartialMatch = false; + string longerTypeName; + string shorterTypeName; - // if the type names are the same length, a partial match is impossible - if (typeName1.Length != typeName2.Length) + // figure out which type name is longer + if (typeName1.Length > typeName2.Length) + { + longerTypeName = typeName1; + shorterTypeName = typeName2; + } + else { - string longerTypeName; - string shorterTypeName; + longerTypeName = typeName2; + shorterTypeName = typeName1; + } - // figure out which type name is longer - if (typeName1.Length > typeName2.Length) - { - longerTypeName = typeName1; - shorterTypeName = typeName2; - } - else - { - longerTypeName = typeName2; - shorterTypeName = typeName1; - } + // if the shorter type name matches the end of the longer one + if (longerTypeName.EndsWith(shorterTypeName, StringComparison.OrdinalIgnoreCase)) + { + int matchIndex = longerTypeName.Length - shorterTypeName.Length; - // if the shorter type name matches the end of the longer one - if (longerTypeName.EndsWith(shorterTypeName, StringComparison.OrdinalIgnoreCase)) + // if the matched sub-string looks like the start of a namespace or class name + if ((longerTypeName[matchIndex - 1] == '.') || (longerTypeName[matchIndex - 1] == '+')) { - int matchIndex = longerTypeName.Length - shorterTypeName.Length; + int precedingBackslashes = 0; - // if the matched sub-string looks like the start of a namespace or class name - if ((longerTypeName[matchIndex - 1] == '.') || (longerTypeName[matchIndex - 1] == '+')) + // confirm there are zero, or an even number of \'s preceding it... + for (int i = matchIndex - 2; i >= 0; i--) { - int precedingBackslashes = 0; - - // confirm there are zero, or an even number of \'s preceding it... - for (int i = matchIndex - 2; i >= 0; i--) + if (longerTypeName[i] == '\\') { - if (longerTypeName[i] == '\\') - { - precedingBackslashes++; - } - else - { - break; - } + precedingBackslashes++; } - - if ((precedingBackslashes % 2) == 0) + else { - isPartialMatch = true; + break; } } + + if ((precedingBackslashes % 2) == 0) + { + isPartialMatch = true; + } } } - else - { - isPartialMatch = (String.Equals(typeName1, typeName2, StringComparison.OrdinalIgnoreCase)); - } - - return isPartialMatch; } + else + { + isPartialMatch = string.Equals(typeName1, typeName2, StringComparison.OrdinalIgnoreCase); + } + + return isPartialMatch; + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + internal LoadedType? Load(string typeName, string assemblyFilePath) + => GetLoadedType(s_cacheOfLoadedTypesByFilter, typeName, assemblyFilePath); + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + private LoadedType? GetLoadedType( + ConcurrentDictionary> cache, + string typeName, + string assemblyFilePath) + { + // A given type filter have been used on a number of assemblies, Based on the type filter + // we will get another dictionary which will map a specific assembly file path to a + // AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly. + var loadInfoToType = cache.GetOrAdd(_isDesiredType, _ => new(StringComparer.OrdinalIgnoreCase)); + + // Get an object which is able to take a type name and determine if it is in the assembly pointed to by the AssemblyInfo. + var typeNameToType = loadInfoToType.GetOrAdd(assemblyFilePath, assemblyFilePath => new(_isDesiredType, assemblyFilePath)); + + return typeNameToType.GetLoadedTypeByTypeName(typeName); + } + /// + /// Given a type filter and an asssemblyInfo object keep track of what types in a given assembly which match the typefilter. + /// Also, use this information to determine if a given TypeName is in the assembly which is pointed to by the AssemblyLoadInfo object. + /// + /// This type represents a combination of a type filter and an assemblyInfo object. + /// + private sealed class TypeCache + { /// - /// Delegate used to log warning messages with formatted string support. + /// Lock to prevent two threads from using this object at the same time. + /// Since we fill up internal structures with what is in the assembly. /// - /// A composite format string for the warning message. - /// An array of objects to format into the warning message. - internal delegate void LogWarningDelegate(string format, params object[] args); + private readonly object _lockObject = new(); /// - /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if - /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type - /// found will be returned. - /// The unusued bool is to match the signature of the Shared copy of TypeLoader. + /// Type filter to pick the correct types out of an assembly. /// - internal LoadedType Load( - string typeName, - AssemblyLoadInfo assembly, - LogWarningDelegate logWarning, - bool useTaskHost = false, - bool taskHostParamsMatchCurrentProc = true) - { - return GetLoadedType(s_cacheOfLoadedTypesByFilter, typeName, assembly); - } + private readonly TypeFilter _isDesiredType; /// - /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if - /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type - /// found will be returned. + /// Assembly file path so we can load an assembly. /// - /// The loaded type, or null if the type was not found. - internal LoadedType ReflectionOnlyLoad( - string typeName, - AssemblyLoadInfo assembly) - { - return GetLoadedType(s_cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly); - } + private readonly string _assemblyFilePath; /// - /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if - /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type - /// found will be returned. + /// What is the type for the given type name, this may be null if the typeName does not map to a type. /// - private LoadedType GetLoadedType(Concurrent.ConcurrentDictionary> cache, string typeName, AssemblyLoadInfo assembly) - { - // A given type filter have been used on a number of assemblies, Based on the type filter we will get another dictionary which - // will map a specific AssemblyLoadInfo to a AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly. - Concurrent.ConcurrentDictionary loadInfoToType = - cache.GetOrAdd(_isDesiredType, (_) => new Concurrent.ConcurrentDictionary()); + private readonly ConcurrentDictionary _typeNameToType; - // Get an object which is able to take a typename and determine if it is in the assembly pointed to by the AssemblyInfo. - AssemblyInfoToLoadedTypes typeNameToType = - loadInfoToType.GetOrAdd(assembly, (_) => new AssemblyInfoToLoadedTypes(_isDesiredType, _)); + /// + /// List of public types in the assembly which match the typefilter and their corresponding types. + /// + private readonly Dictionary _publicTypeNameToType; - return typeNameToType.GetLoadedTypeByTypeName(typeName); - } + /// + /// Have we scanned the public types for this assembly yet. + /// + private long _haveScannedPublicTypes; /// - /// Given a type filter and an asssemblyInfo object keep track of what types in a given assembly which match the typefilter. - /// Also, use this information to determine if a given TypeName is in the assembly which is pointed to by the AssemblyLoadInfo object. - /// - /// This type represents a combination of a type filter and an assemblyInfo object. + /// If we loaded an assembly for this type. + /// We use this information to set the LoadedType.LoadedAssembly so that this object can be used + /// to help created AppDomains to resolve those that it could not load successfully. /// - private class AssemblyInfoToLoadedTypes - { - /// - /// Lock to prevent two threads from using this object at the same time. - /// Since we fill up internal structures with what is in the assembly - /// - private readonly LockType _lockObject = new(); - - /// - /// Type filter to pick the correct types out of an assembly - /// - private TypeFilter _isDesiredType; - - /// - /// Assembly load information so we can load an assembly - /// - private AssemblyLoadInfo _assemblyLoadInfo; - - /// - /// What is the type for the given type name, this may be null if the typeName does not map to a type. - /// - private Concurrent.ConcurrentDictionary _typeNameToType; - - /// - /// List of public types in the assembly which match the typefilter and their corresponding types - /// - private Dictionary _publicTypeNameToType; - - /// - /// Have we scanned the public types for this assembly yet. - /// - private long _haveScannedPublicTypes; - - /// - /// If we loaded an assembly for this type. - /// We use this information to set the LoadedType.LoadedAssembly so that this object can be used - /// to help created AppDomains to resolve those that it could not load successfuly - /// - private Assembly _loadedAssembly; - - /// - /// Given a type filter, and an assembly to load the type information from determine if a given type name is in the assembly or not. - /// - internal AssemblyInfoToLoadedTypes(TypeFilter typeFilter, AssemblyLoadInfo loadInfo) - { - ErrorUtilities.VerifyThrowArgumentNull(typeFilter, "typefilter"); - ErrorUtilities.VerifyThrowArgumentNull(loadInfo); + private Assembly? _loadedAssembly; - _isDesiredType = typeFilter; - _assemblyLoadInfo = loadInfo; - _typeNameToType = new Concurrent.ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _publicTypeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + internal TypeCache(TypeFilter typeFilter, string assemblyFilePath) + { + ErrorUtilities.VerifyThrowArgumentNull(typeFilter); + ErrorUtilities.VerifyThrowArgumentNull(assemblyFilePath); - /// - /// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly - /// - internal LoadedType GetLoadedTypeByTypeName(string typeName) - { - ErrorUtilities.VerifyThrowArgumentNull(typeName); + _isDesiredType = typeFilter; + _assemblyFilePath = assemblyFilePath; + _typeNameToType = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _publicTypeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - // Only one thread should be doing operations on this instance of the object at a time. + /// + /// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly + /// + internal LoadedType? GetLoadedTypeByTypeName(string typeName) + { + ErrorUtilities.VerifyThrowArgumentNull(typeName); - Type type = _typeNameToType.GetOrAdd(typeName, (key) => + // Only one thread should be doing operations on this instance of the object at a time. + Type? type = _typeNameToType.GetOrAdd(typeName, (key) => + { + if (Interlocked.Read(ref _haveScannedPublicTypes) == 0) { - if ((_assemblyLoadInfo.AssemblyName != null) && (typeName.Length > 0)) + lock (_lockObject) { - try + if (Interlocked.Read(ref _haveScannedPublicTypes) == 0) { - // try to load the type using its assembly qualified name - Type t2 = Type.GetType(typeName + "," + _assemblyLoadInfo.AssemblyName, false /* don't throw on error */, true /* case-insensitive */); - if (t2 != null) - { - return !_isDesiredType(t2, null) ? null : t2; - } - } - catch (ArgumentException) - { - // Type.GetType() will throw this exception if the type name is invalid -- but we have no idea if it's the - // type or the assembly name that's the problem -- so just ignore the exception, because we're going to - // check the existence/validity of the assembly and type respectively, below anyway + ScanAssemblyForPublicTypes(); + Interlocked.Exchange(ref _haveScannedPublicTypes, ~0); } } + } - if (Interlocked.Read(ref _haveScannedPublicTypes) == 0) + foreach (KeyValuePair desiredTypeInAssembly in _publicTypeNameToType) + { + // if type matches partially on its name + if (typeName.Length == 0 || IsPartialTypeNameMatch(desiredTypeInAssembly.Key, typeName)) { - lock (_lockObject) - { - if (Interlocked.Read(ref _haveScannedPublicTypes) == 0) - { - ScanAssemblyForPublicTypes(); - Interlocked.Exchange(ref _haveScannedPublicTypes, ~0); - } - } + return desiredTypeInAssembly.Value; } + } - foreach (KeyValuePair desiredTypeInAssembly in _publicTypeNameToType) - { - // if type matches partially on its name - if (typeName.Length == 0 || IsPartialTypeNameMatch(desiredTypeInAssembly.Key, typeName)) - { - return desiredTypeInAssembly.Value; - } - } + return null; + }); - return null; - }); + return type != null + ? new LoadedType(type, _assemblyFilePath, _loadedAssembly ?? type.Assembly) + : null; + } - return type != null ? new LoadedType(type, _assemblyLoadInfo, _loadedAssembly ?? type.Assembly, typeof(ITaskItem)) : null; + /// + /// Scan the assembly pointed to by the assemblyLoadInfo for public types. We will use these public types to do partial name matching on + /// to find tasks, loggers, and task factories. + /// + private void ScanAssemblyForPublicTypes() + { + // we need to search the assembly for the type... + try + { + _loadedAssembly = Assembly.LoadFrom(_assemblyFilePath); } - - /// - /// Scan the assembly pointed to by the assemblyLoadInfo for public types. We will use these public types to do partial name matching on - /// to find tasks, loggers, and task factories. - /// - private void ScanAssemblyForPublicTypes() + catch (ArgumentException e) { - // we need to search the assembly for the type... - try - { - if (_assemblyLoadInfo.AssemblyName != null) - { - _loadedAssembly = Assembly.Load(_assemblyLoadInfo.AssemblyName); - } - else - { - _loadedAssembly = Assembly.LoadFrom(_assemblyLoadInfo.AssemblyFile); - } - } - catch (ArgumentException e) - { - // Assembly.Load() and Assembly.LoadFrom() will throw an ArgumentException if the assembly name is invalid - // convert to a FileNotFoundException because it's more meaningful - // NOTE: don't use ErrorUtilities.VerifyThrowFileExists() here because that will hit the disk again - throw new FileNotFoundException(null, _assemblyLoadInfo.AssemblyLocation, e); - } + // Assembly.Load() and Assembly.LoadFrom() will throw an ArgumentException if the assembly name is invalid + // convert to a FileNotFoundException because it's more meaningful + // NOTE: don't use ErrorUtilities.VerifyThrowFileExists() here because that will hit the disk again + throw new FileNotFoundException(message: null, _assemblyFilePath, e); + } - // only look at public types - Type[] allPublicTypesInAssembly = _loadedAssembly.GetExportedTypes(); - foreach (Type publicType in allPublicTypesInAssembly) + // only look at public types + Type[] allPublicTypesInAssembly = _loadedAssembly.GetExportedTypes(); + foreach (Type publicType in allPublicTypesInAssembly) + { + if (_isDesiredType(publicType, filterCriteria: null)) { - if (_isDesiredType(publicType, null)) - { - _publicTypeNameToType.Add(publicType.FullName, publicType); - } + _publicTypeNameToType.Add(publicType.FullName, publicType); } } } diff --git a/src/MSBuildTaskHost/Utilities/EnvironmentUtilities.cs b/src/MSBuildTaskHost/Utilities/EnvironmentUtilities.cs new file mode 100644 index 00000000000..bd2ef36d277 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/EnvironmentUtilities.cs @@ -0,0 +1,75 @@ +// 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.Threading; + +namespace Microsoft.Build.TaskHost.Utilities; + +internal static partial class EnvironmentUtilities +{ + private static volatile int s_processId; + private static volatile string? s_processPath; + private static int? s_processSessionId; + + /// Gets the unique identifier for the current process. + public static int CurrentProcessId + { + get + { + // copied from Environment.ProcessId + int processId = s_processId; + if (processId == 0) + { + using Process currentProcess = Process.GetCurrentProcess(); + s_processId = processId = currentProcess.Id; + + // Assume that process Id zero is invalid for user processes. It holds for all mainstream operating systems. + Debug.Assert(processId != 0); + } + + return processId; + } + } + + /// + /// Returns the path of the executable that started the currently executing process. Returns null when the path is not available. + /// + /// Path of the executable that started the currently executing process + /// + /// If the executable is renamed or deleted before this property is first accessed, the return value is undefined and depends on the operating system. + /// + public static string? ProcessPath + { + get + { + // copied from Environment.ProcessPath + string? processPath = s_processPath; + if (processPath == null) + { + // The value is cached both as a performance optimization and to ensure that the API always returns + // the same path in a given process. + using Process currentProcess = Process.GetCurrentProcess(); + Interlocked.CompareExchange(ref s_processPath, currentProcess.MainModule.FileName ?? "", null); + processPath = s_processPath; + Debug.Assert(processPath != null); + } + + return (processPath?.Length != 0) ? processPath : null; + } + } + + public static int ProcessSessionId + { + get + { + return s_processSessionId ??= GetProcessSessionId(); + + static int GetProcessSessionId() + { + using Process currentProcess = Process.GetCurrentProcess(); + return currentProcess.SessionId; + } + } + } +} diff --git a/src/MSBuildTaskHost/Utilities/ErrorUtilities.cs b/src/MSBuildTaskHost/Utilities/ErrorUtilities.cs new file mode 100644 index 00000000000..a2252e561b3 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/ErrorUtilities.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Build.TaskHost.Exceptions; +using Microsoft.Build.TaskHost.Resources; + +namespace Microsoft.Build.TaskHost.Utilities; + +/// +/// This class contains methods that are useful for error checking and validation. +/// +internal static class ErrorUtilities +{ + [DoesNotReturn] + internal static void ThrowInternalError(string message) + => throw new InternalErrorException(message); + + [DoesNotReturn] + internal static void ThrowInternalError(string format, object? arg0) + => throw new InternalErrorException(string.Format(format, arg0)); + + /// + /// Throws InternalErrorException. + /// Indicates the code path followed should not have been possible. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + [DoesNotReturn] + internal static void ThrowInternalErrorUnreachable() + => throw new InternalErrorException("Unreachable?"); + + /// + /// Helper to throw an InternalErrorException when the specified parameter is null. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// The value of the argument. + /// Parameter that should not be null + internal static void VerifyThrowInternalNull( + [NotNull] object? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) + { + if (parameter is null) + { + ThrowInternalError($"{parameterName} unexpectedly null"); + } + } + + /// + /// Helper to throw an InternalErrorException when the specified parameter is null or zero length. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// The value of the argument. + /// Parameter that should not be null or zero length + internal static void VerifyThrowInternalLength( + [NotNull] string? parameterValue, + [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) + { + VerifyThrowInternalNull(parameterValue, parameterName); + + if (parameterValue.Length == 0) + { + ThrowInternalError($"{parameterName} unexpectedly empty"); + } + } + + /// + /// This method should be used in places where one would normally put + /// an "assert". It should be used to validate that our assumptions are + /// true, where false would indicate that there must be a bug in our + /// code somewhere. This should not be used to throw errors based on bad + /// user input or anything that the user did wrong. + /// + internal static void VerifyThrow([DoesNotReturnIf(false)] bool condition, string message) + { + if (!condition) + { + ThrowInternalError(message); + } + } + + /// + /// Overload for one string format argument. + /// + internal static void VerifyThrow([DoesNotReturnIf(false)] bool condition, string format, object? arg0) + { + if (!condition) + { + ThrowInternalError(format, arg0); + } + } + + /// + /// Throws an InvalidOperationException with the specified resource string + /// + /// Resource to use in the exception + /// Formatting args. + [DoesNotReturn] + internal static void ThrowInvalidOperation(string format, object? arg0, object? arg1, object? arg2) + => throw new InvalidOperationException(string.Format(format, arg0, arg1, arg2)); + + /// + /// Throws an ArgumentException that can include an inner exception. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + /// + /// This method is thread-safe. + /// + /// Can be null. + /// + /// + [DoesNotReturn] + private static void ThrowArgument(Exception? innerException, string format, object? arg0) + => throw new ArgumentException(string.Format(format, arg0), innerException); + + /// + /// Overload for one string format argument. + /// + internal static void VerifyThrowArgument([DoesNotReturnIf(false)] bool condition, string format, object? arg0) + => VerifyThrowArgument(condition, innerException: null, format, arg0); + + /// + /// Overload for one string format argument. + /// + internal static void VerifyThrowArgument( + [DoesNotReturnIf(false)] bool condition, Exception? innerException, string format, object? arg0) + { + if (!condition) + { + ThrowArgument(innerException, format, arg0); + } + } + + /// + /// Throws an ArgumentNullException if the given string parameter is null + /// and ArgumentException if it has zero length. + /// + internal static void VerifyThrowArgumentLength( + [NotNull] string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) + { + VerifyThrowArgumentNull(parameter, parameterName); + + if (parameter.Length == 0) + { + ThrowArgumentLength(parameterName); + } + } + + [DoesNotReturn] + private static void ThrowArgumentLength(string? parameterName) + => throw new ArgumentException(string.Format(SR.Shared_ParameterCannotHaveZeroLength, parameterName), parameterName); + + /// + /// Throws an ArgumentNullException if the given parameter is null. + /// + internal static void VerifyThrowArgumentNull( + [NotNull] object? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) + { + if (parameter is null) + { + ThrowArgumentNull(parameterName, SR.Shared_ParameterCannotBeNull); + } + } + + [DoesNotReturn] + private static void ThrowArgumentNull(string? parameterName, string message) + => throw new ArgumentNullException(parameterName, string.Format(message, parameterName)); +} diff --git a/src/MSBuildTaskHost/Utilities/EscapingUtilities.cs b/src/MSBuildTaskHost/Utilities/EscapingUtilities.cs new file mode 100644 index 00000000000..1e2a3941b82 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/EscapingUtilities.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; +using System.Collections.Generic; +using System.Text; +using Microsoft.NET.StringTools; + +namespace Microsoft.Build.TaskHost.Utilities; + +/// +/// This class implements static methods to assist with unescaping of %XX codes +/// in the MSBuild file format. +/// +/// +/// PERF: since we escape and unescape relatively frequently, it may be worth caching +/// the last N strings that were (un)escaped +/// +internal static class EscapingUtilities +{ + /// + /// Special characters that need escaping. + /// It's VERY important that the percent character is the FIRST on the list - since it's both a character + /// we escape and use in escape sequences, we can unintentionally escape other escape sequences if we + /// don't process it first. Of course we'll have a similar problem if we ever decide to escape hex digits + /// (that would require rewriting the algorithm) but since it seems unlikely that we ever do, this should + /// be good enough to avoid complicating the algorithm at this point. + /// + private static readonly char[] s_charsToEscape = ['%', '*', '?', '@', '$', '(', ')', ';', '\'']; + + /// + /// Optional cache of escaped strings for use when needing to escape in performance-critical scenarios with significant + /// expected string reuse. + /// + private static readonly Dictionary s_unescapedToEscapedStrings = new(StringComparer.Ordinal); + + private static bool TryDecodeHexDigit(char character, out int value) + { + switch (character) + { + case >= '0' and <= '9': + value = character - '0'; + return true; + case >= 'A' and <= 'F': + value = character - 'A' + 10; + return true; + case >= 'a' and <= 'f': + value = character - 'a' + 10; + return true; + } + + value = default; + return false; + } + + /// + /// Replaces all instances of %XX in the input string with the character represented + /// by the hexadecimal number XX. + /// + /// The string to unescape. + /// If the string should be trimmed before being unescaped. + /// unescaped string + internal static string UnescapeAll(string escapedString, bool trim = false) + { + // If the string doesn't contain anything, then by definition it doesn't + // need unescaping. + if (string.IsNullOrEmpty(escapedString)) + { + return escapedString; + } + + // If there are no percent signs, just return the original string immediately. + // Don't even instantiate the StringBuilder. + int indexOfPercent = escapedString.IndexOf('%'); + if (indexOfPercent == -1) + { + return trim ? escapedString.Trim() : escapedString; + } + + // This is where we're going to build up the final string to return to the caller. + StringBuilder unescapedString = StringBuilderCache.Acquire(escapedString.Length); + + int currentPosition = 0; + int escapedStringLength = escapedString.Length; + if (trim) + { + while (currentPosition < escapedString.Length && char.IsWhiteSpace(escapedString[currentPosition])) + { + currentPosition++; + } + + if (currentPosition == escapedString.Length) + { + return string.Empty; + } + + while (char.IsWhiteSpace(escapedString[escapedStringLength - 1])) + { + escapedStringLength--; + } + } + + // Loop until there are no more percent signs in the input string. + while (indexOfPercent != -1) + { + // There must be two hex characters following the percent sign + // for us to even consider doing anything with this. + if ((indexOfPercent <= (escapedStringLength - 3)) && + TryDecodeHexDigit(escapedString[indexOfPercent + 1], out int digit1) && + TryDecodeHexDigit(escapedString[indexOfPercent + 2], out int digit2)) + { + // First copy all the characters up to the current percent sign into + // the destination. + unescapedString.Append(escapedString, currentPosition, indexOfPercent - currentPosition); + + // Convert the %XX to an actual real character. + char unescapedCharacter = (char)((digit1 << 4) + digit2); + + // if the unescaped character is not on the exception list, append it + unescapedString.Append(unescapedCharacter); + + // Advance the current pointer to reflect the fact that the destination string + // is up to date with everything up to and including this escape code we just found. + currentPosition = indexOfPercent + 3; + } + + // Find the next percent sign. + indexOfPercent = escapedString.IndexOf('%', indexOfPercent + 1); + } + + // Okay, there are no more percent signs in the input string, so just copy the remaining + // characters into the destination. + unescapedString.Append(escapedString, currentPosition, escapedStringLength - currentPosition); + + return StringBuilderCache.GetStringAndRelease(unescapedString); + } + + /// + /// Adds instances of %XX in the input string where the char to be escaped appears + /// XX is the hex value of the ASCII code for the char. Interns and caches the result. + /// + /// + /// NOTE: Only recommended for use in scenarios where there's expected to be significant + /// repetition of the escaped string. Cache currently grows unbounded. + /// + internal static string EscapeWithCaching(string unescapedString) + => EscapeWithOptionalCaching(unescapedString, cache: true); + + /// + /// Adds instances of %XX in the input string where the char to be escaped appears + /// XX is the hex value of the ASCII code for the char. + /// + /// The string to escape. + /// escaped string + internal static string Escape(string unescapedString) + => EscapeWithOptionalCaching(unescapedString, cache: false); + + /// + /// Adds instances of %XX in the input string where the char to be escaped appears + /// XX is the hex value of the ASCII code for the char. Caches if requested. + /// + /// The string to escape. + /// + /// True if the cache should be checked, and if the resultant string + /// should be cached. + /// + private static string EscapeWithOptionalCaching(string unescapedString, bool cache) + { + // If there are no special chars, just return the original string immediately. + // Don't even instantiate the StringBuilder. + if (string.IsNullOrEmpty(unescapedString) || !ContainsReservedCharacters(unescapedString)) + { + return unescapedString; + } + + // next, if we're caching, check to see if it's already there. + if (cache) + { + lock (s_unescapedToEscapedStrings) + { + if (s_unescapedToEscapedStrings.TryGetValue(unescapedString, out string? cachedEscapedString)) + { + return cachedEscapedString; + } + } + } + + // This is where we're going to build up the final string to return to the caller. + StringBuilder escapedStringBuilder = StringBuilderCache.Acquire(unescapedString.Length * 2); + + AppendEscapedString(escapedStringBuilder, unescapedString); + + if (!cache) + { + return StringBuilderCache.GetStringAndRelease(escapedStringBuilder); + } + + string escapedString = Strings.WeakIntern(escapedStringBuilder.ToString()); + StringBuilderCache.Release(escapedStringBuilder); + + lock (s_unescapedToEscapedStrings) + { + s_unescapedToEscapedStrings[unescapedString] = escapedString; + } + + return escapedString; + } + + /// + /// Before trying to actually escape the string, it can be useful to call this method to determine + /// if escaping is necessary at all. This can save lots of calls to copy around item metadata + /// that is really the same whether escaped or not. + /// + /// + /// + private static bool ContainsReservedCharacters(string unescapedString) + => unescapedString.IndexOfAny(s_charsToEscape) >= 0; + + /// + /// Convert the given integer into its hexadecimal representation. + /// + /// The number to convert, which must be non-negative and less than 16 + /// The character which is the hexadecimal representation of . + private static char HexDigitChar(int x) + => (char)(x + (x < 10 ? '0' : ('a' - 10))); + + /// + /// Append the escaped version of the given character to a . + /// + /// The to which to append. + /// The character to escape. + private static void AppendEscapedChar(StringBuilder sb, char ch) + { + // Append the escaped version which is a percent sign followed by two hexadecimal digits + sb.Append('%'); + sb.Append(HexDigitChar(ch / 0x10)); + sb.Append(HexDigitChar(ch & 0x0F)); + } + + /// + /// Append the escaped version of the given string to a . + /// + /// The to which to append. + /// The unescaped string. + private static void AppendEscapedString(StringBuilder sb, string unescapedString) + { + // Replace each unescaped special character with an escape sequence one + for (int idx = 0; ;) + { + int nextIdx = unescapedString.IndexOfAny(s_charsToEscape, idx); + if (nextIdx == -1) + { + sb.Append(unescapedString, idx, unescapedString.Length - idx); + break; + } + + sb.Append(unescapedString, idx, nextIdx - idx); + AppendEscapedChar(sb, unescapedString[nextIdx]); + idx = nextIdx + 1; + } + } +} diff --git a/src/MSBuildTaskHost/Utilities/ExceptionHandling.cs b/src/MSBuildTaskHost/Utilities/ExceptionHandling.cs new file mode 100644 index 00000000000..bd5a5e976c3 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/ExceptionHandling.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Security; +using System.Threading; +using Microsoft.Build.TaskHost.Exceptions; + +namespace Microsoft.Build.TaskHost.Utilities; + +/// +/// Utility methods for classifying and handling exceptions. +/// +internal static class ExceptionHandling +{ + /// + /// The directory used for diagnostic log files. + /// + public static string DebugDumpPath { get; } = GetDebugDumpPath(); + + /// + /// Gets the location of the directory used for diagnostic log files. + /// + /// + private static string GetDebugDumpPath() + { + string debugPath = Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); + + return !string.IsNullOrEmpty(debugPath) + ? debugPath + : FileUtilities.TempFileDirectory; + } + + /// + /// The filename that exceptions will be dumped to. + /// + private static string? s_dumpFileName; + + /// + /// If the given exception is "ignorable under some circumstances" return false. + /// Otherwise it's "really bad", and return true. + /// This makes it possible to catch(Exception ex) without catching disasters. + /// + /// The exception to check. + /// True if exception is critical. + internal static bool IsCriticalException(Exception e) + => e is OutOfMemoryException + or StackOverflowException + or ThreadAbortException + or ThreadInterruptedException + or AccessViolationException + or InternalErrorException; + + /// + /// Determine whether the exception is file-IO related. + /// + /// The exception to check. + /// True if exception is IO related. + internal static bool IsIoRelatedException(Exception e) + // These all derive from IOException + // DirectoryNotFoundException + // DriveNotFoundException + // EndOfStreamException + // FileLoadException + // FileNotFoundException + // PathTooLongException + // PipeException + => e is UnauthorizedAccessException + or NotSupportedException + or (ArgumentException and not ArgumentNullException) + or SecurityException + or IOException; + + /// + /// Dump any unhandled exceptions to a file so they can be diagnosed. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It is called by the CLR")] + internal static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) + => DumpExceptionToFile((Exception)e.ExceptionObject); + + /// + /// Dump the exception information to a file. + /// + internal static void DumpExceptionToFile(Exception exception) + { + try + { + // Locking on a type is not recommended. However, we are doing it here to be extra cautious about compatibility because + // this method previously had a [MethodImpl(MethodImplOptions.Synchronized)] attribute, which does lock on the type when + // applied to a static method. + lock (typeof(ExceptionHandling)) + { + if (s_dumpFileName == null) + { + Guid guid = Guid.NewGuid(); + + // For some reason we get Watson buckets because GetTempPath gives us a folder here that doesn't exist. + // Either because %TMP% is misdefined, or because they deleted the temp folder during the build. + // If this throws, no sense catching it, we can't log it now, and we're here + // because we're a child node with no console to log to, so die + Directory.CreateDirectory(DebugDumpPath); + + var pid = EnvironmentUtilities.CurrentProcessId; + + // This naming pattern is assumed in ReadAnyExceptionFromFile + s_dumpFileName = Path.Combine(DebugDumpPath, $"MSBuild_pid-{pid}_{guid:n}.failure.txt"); + + using (StreamWriter writer = FileUtilities.CreateWriterForAppend(s_dumpFileName)) + { + writer.WriteLine("UNHANDLED EXCEPTIONS FROM PROCESS {0}:", pid); + writer.WriteLine("====================="); + } + } + + using (StreamWriter writer = FileUtilities.CreateWriterForAppend(s_dumpFileName)) + { + // "G" format is, e.g., 6/15/2008 9:15:07 PM + writer.WriteLine(DateTime.Now.ToString("G", CultureInfo.CurrentCulture)); + writer.WriteLine(exception.ToString()); + writer.WriteLine("==================="); + } + } + } + catch + { + // Some customers experience exceptions such as 'OutOfMemory' errors when msbuild attempts to log errors to a local file. + // This catch helps to prevent the application from crashing in this best-effort dump-diagnostics path, + // but doesn't prevent the overall crash from going to Watson. + } + } +} diff --git a/src/MSBuildTaskHost/Utilities/FileUtilities.ItemSpecModifiers.cs b/src/MSBuildTaskHost/Utilities/FileUtilities.ItemSpecModifiers.cs new file mode 100644 index 00000000000..d01bde782c1 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/FileUtilities.ItemSpecModifiers.cs @@ -0,0 +1,435 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.Build.TaskHost.Resources; + +namespace Microsoft.Build.TaskHost.Utilities; + +internal static partial class FileUtilities +{ + /// + /// Encapsulates the definitions of the item-spec modifiers a.k.a. reserved item metadata. + /// + internal static class ItemSpecModifiers + { + internal const string FullPath = "FullPath"; + internal const string RootDir = "RootDir"; + internal const string Filename = "Filename"; + internal const string Extension = "Extension"; + internal const string RelativeDir = "RelativeDir"; + internal const string Directory = "Directory"; + internal const string RecursiveDir = "RecursiveDir"; + internal const string Identity = "Identity"; + internal const string ModifiedTime = "ModifiedTime"; + internal const string CreatedTime = "CreatedTime"; + internal const string AccessedTime = "AccessedTime"; + internal const string DefiningProjectFullPath = "DefiningProjectFullPath"; + internal const string DefiningProjectDirectory = "DefiningProjectDirectory"; + internal const string DefiningProjectName = "DefiningProjectName"; + internal const string DefiningProjectExtension = "DefiningProjectExtension"; + + // These are all the well-known attributes. + internal static readonly string[] All = + [ + FullPath, + RootDir, + Filename, + Extension, + RelativeDir, + Directory, + RecursiveDir, // <-- Not derivable. + Identity, + ModifiedTime, + CreatedTime, + AccessedTime, + DefiningProjectFullPath, + DefiningProjectDirectory, + DefiningProjectName, + DefiningProjectExtension + ]; + + private enum ItemSpecModifierKind + { + FullPath, + RootDir, + Filename, + Extension, + RelativeDir, + Directory, + RecursiveDir, // <-- Not derivable. + Identity, + ModifiedTime, + CreatedTime, + AccessedTime, + DefiningProjectFullPath, + DefiningProjectDirectory, + DefiningProjectName, + DefiningProjectExtension + } + + private static readonly Dictionary s_itemSpecModifierMap = new(StringComparer.OrdinalIgnoreCase) + { + { FullPath, ItemSpecModifierKind.FullPath }, + { RootDir, ItemSpecModifierKind.RootDir }, + { Filename, ItemSpecModifierKind.Filename }, + { Extension, ItemSpecModifierKind.Extension }, + { RelativeDir, ItemSpecModifierKind.RelativeDir }, + { Directory, ItemSpecModifierKind.Directory }, + { RecursiveDir, ItemSpecModifierKind.RecursiveDir }, + { Identity, ItemSpecModifierKind.Identity }, + { ModifiedTime, ItemSpecModifierKind.ModifiedTime }, + { CreatedTime, ItemSpecModifierKind.CreatedTime }, + { AccessedTime, ItemSpecModifierKind.AccessedTime }, + { DefiningProjectFullPath, ItemSpecModifierKind.DefiningProjectFullPath }, + { DefiningProjectDirectory, ItemSpecModifierKind.DefiningProjectDirectory }, + { DefiningProjectName, ItemSpecModifierKind.DefiningProjectName }, + { DefiningProjectExtension, ItemSpecModifierKind.DefiningProjectExtension } + }; + + /// + /// Indicates if the given name is reserved for an item-spec modifier. + /// + internal static bool IsItemSpecModifier([NotNullWhen(true)] string name) + => name != null && s_itemSpecModifierMap.ContainsKey(name); + + /// + /// Indicates if the given name is reserved for a derivable item-spec modifier. + /// Derivable means it can be computed given a file name. + /// + /// Name to check. + /// true, if name of a derivable modifier + internal static bool IsDerivableItemSpecModifier(string name) + { + bool isItemSpecModifier = IsItemSpecModifier(name); + + if (isItemSpecModifier && name.Length == 12 && name[0] is 'R' or 'r') + { + // The only 12 letter ItemSpecModifier that starts with 'R' is 'RecursiveDir' + return false; + } + + return isItemSpecModifier; + } + + /// + /// Performs path manipulations on the given item-spec as directed. + /// + /// Supported modifiers: + /// %(FullPath) = full path of item + /// %(RootDir) = root directory of item + /// %(Filename) = item filename without extension + /// %(Extension) = item filename extension + /// %(RelativeDir) = item directory as given in item-spec + /// %(Directory) = full path of item directory relative to root + /// %(RecursiveDir) = portion of item path that matched a recursive wildcard + /// %(Identity) = item-spec as given + /// %(ModifiedTime) = last write time of item + /// %(CreatedTime) = creation time of item + /// %(AccessedTime) = last access time of item + /// + /// NOTES: + /// 1) This method always returns an empty string for the %(RecursiveDir) modifier because it does not have enough + /// information to compute it -- only the BuildItem class can compute this modifier. + /// 2) All but the file time modifiers could be cached, but it's not worth the space. Only full path is cached, as the others are just string manipulations. + /// + /// + /// Methods of the Path class "normalize" slashes and periods. For example: + /// 1) successive slashes are combined into 1 slash + /// 2) trailing periods are discarded + /// 3) forward slashes are changed to back-slashes + /// + /// As a result, we cannot rely on any file-spec that has passed through a Path method to remain the same. We will + /// therefore not bother preserving slashes and periods when file-specs are transformed. + /// + /// Never returns null. + /// + /// The root directory for relative item-specs. When called on the Engine thread, this is the project directory. When called as part of building a task, it is null, indicating that the current directory should be used. + /// The item-spec to modify. + /// The path to the project that defined this item (may be null). + /// The modifier to apply to the item-spec. + /// Full path if any was previously computed, to cache. + /// The modified item-spec (can be empty string, but will never be null). + /// Thrown when the item-spec is not a path. + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Pre-existing")] + internal static string GetItemSpecModifier( + string? currentDirectory, + string itemSpec, + string? definingProjectEscaped, + string modifier, + ref string? fullPath) + { + ErrorUtilities.VerifyThrow(itemSpec != null, "Need item-spec to modify."); + ErrorUtilities.VerifyThrow(modifier != null, "Need modifier to apply to item-spec."); + + try + { + if (s_itemSpecModifierMap.TryGetValue(modifier, out ItemSpecModifierKind kind)) + { + switch (kind) + { + case ItemSpecModifierKind.FullPath: + ComputeFullPath(itemSpec, currentDirectory, ref fullPath); + return fullPath; + + case ItemSpecModifierKind.RootDir: + ComputeFullPath(itemSpec, currentDirectory, ref fullPath); + return ComputeRootDir(fullPath); + + case ItemSpecModifierKind.Filename: + return ComputeFileName(itemSpec); + + case ItemSpecModifierKind.Extension: + return ComputeExtension(itemSpec); + + case ItemSpecModifierKind.RelativeDir: + return ComputeRelativeDir(itemSpec); + + case ItemSpecModifierKind.Directory: + ComputeFullPath(itemSpec, currentDirectory, ref fullPath); + return ComputeDirectory(fullPath); + + case ItemSpecModifierKind.RecursiveDir: + // only the BuildItem class can compute this modifier -- so leave empty + return string.Empty; + + case ItemSpecModifierKind.Identity: + return itemSpec; + + case ItemSpecModifierKind.ModifiedTime: + return ComputeModifiedTime(itemSpec); + + case ItemSpecModifierKind.CreatedTime: + return ComputeCreatedTime(itemSpec); + + case ItemSpecModifierKind.AccessedTime: + return ComputeAccessedTime(itemSpec); + + case ItemSpecModifierKind.DefiningProjectDirectory: + case ItemSpecModifierKind.DefiningProjectFullPath: + case ItemSpecModifierKind.DefiningProjectName: + case ItemSpecModifierKind.DefiningProjectExtension: + if (string.IsNullOrEmpty(definingProjectEscaped)) + { + // We have nothing to work with, but that's sometimes OK -- so just return String.Empty + return string.Empty; + } + + ErrorUtilities.VerifyThrow(definingProjectEscaped != null, $"{nameof(definingProjectEscaped)} is null."); + + switch (kind) + { + case ItemSpecModifierKind.DefiningProjectDirectory: + return ComputeDefiningProjectDirectory(definingProjectEscaped, currentDirectory); + + case ItemSpecModifierKind.DefiningProjectFullPath: + return ComputeDefiningProjectFullPath(definingProjectEscaped, currentDirectory); + + case ItemSpecModifierKind.DefiningProjectName: + return ComputeDefiningProjectFileName(definingProjectEscaped); + + case ItemSpecModifierKind.DefiningProjectExtension: + return ComputeDefiningProjectExtension(definingProjectEscaped); + } + + break; + } + } + } + catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) + { + ErrorUtilities.ThrowInvalidOperation(SR.Shared_InvalidFilespecForTransform, modifier, itemSpec, e.Message); + } + + ErrorUtilities.ThrowInternalError($"\"{modifier}\" is not a valid item-spec modifier."); + return null; + } + + private static string ComputeFullPath(string itemSpec, string? currentDirectory) + { + currentDirectory ??= string.Empty; + string fullPath = GetFullPath(itemSpec, currentDirectory); + + ThrowForUrl(fullPath, itemSpec, currentDirectory); + + return fullPath; + } + + private static void ComputeFullPath(string itemSpec, string? currentDirectory, [NotNull] ref string? existingFullPath) + { + if (existingFullPath != null) + { + return; + } + + existingFullPath = ComputeFullPath(itemSpec, currentDirectory); + } + + private static string ComputeRootDir(string fullPath) + { + string rootDir = Path.GetPathRoot(fullPath); + + if (!EndsWithSlash(rootDir)) + { + ErrorUtilities.VerifyThrow( + StartsWithUncPattern(rootDir), + "Only UNC shares should be missing trailing slashes."); + + // restore/append trailing slash if Path.GetPathRoot() has either removed it, or failed to add it + // (this happens with UNC shares) + rootDir += Path.DirectorySeparatorChar; + } + + return rootDir; + } + + private static string ComputeFileName(string itemSpec) + // if the item-spec is a root directory, it can have no filename + // NOTE: this is to prevent Path.GetFileNameWithoutExtension() from treating server and share elements + // in a UNC file-spec as filenames e.g. \\server, \\server\share + => IsRootDirectory(itemSpec) + ? string.Empty + : Path.GetFileNameWithoutExtension(itemSpec); + + private static string ComputeExtension(string itemSpec) + // if the item-spec is a root directory, it can have no extension + // NOTE: this is to prevent Path.GetFileNameWithoutExtension() from treating server and share elements + // in a UNC file-spec as filenames e.g. \\server, \\server\share + => IsRootDirectory(itemSpec) + ? string.Empty + : Path.GetExtension(itemSpec); + + private static string ComputeRelativeDir(string itemSpec) + => GetDirectory(itemSpec); + + private static string ComputeDirectory(string fullPath) + { + string directory = GetDirectory(fullPath); + + int length = StartsWithDrivePattern(directory) + ? 2 + : StartsWithUncPatternMatchLength(directory); + + if (length != -1) + { + ErrorUtilities.VerifyThrow( + directory.Length > length && IsAnySlash(directory[length]), + "Root directory must have a trailing slash."); + + directory = directory.Substring(length + 1); + } + + return directory; + } + + private static string ComputeModifiedTime(string itemSpec) + { + // About to go out to the filesystem. This means data is leaving the engine, so need to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + return NativeMethods.FileExists(unescapedItemSpec) + ? File.GetLastWriteTime(unescapedItemSpec).ToString(FileTimeFormat, provider: null) + : string.Empty; // File does not exist, or path is a directory + } + + private static string ComputeCreatedTime(string itemSpec) + { + // About to go out to the filesystem. This means data is leaving the engine, so need to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + return NativeMethods.FileExists(unescapedItemSpec) + ? File.GetCreationTime(unescapedItemSpec).ToString(FileTimeFormat, provider: null) + : string.Empty; // File does not exist, or path is a directory + } + + private static string ComputeAccessedTime(string itemSpec) + { + // About to go out to the filesystem. This means data is leaving the engine, so need to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + return NativeMethods.FileExists(unescapedItemSpec) + ? File.GetLastAccessTime(unescapedItemSpec).ToString(FileTimeFormat, provider: null) + : string.Empty; // File does not exist, or path is a directory + } + + private static string ComputeDefiningProjectDirectory(string itemSpec, string? currentDirectory) + { + string fullPath = ComputeFullPath(itemSpec, currentDirectory); + string rootDir = ComputeRootDir(fullPath); + string directory = ComputeDirectory(fullPath); + + return Path.Combine(rootDir, directory); + } + + private static string ComputeDefiningProjectFullPath(string itemSpec, string? currentDirectory) + => ComputeFullPath(itemSpec, currentDirectory); + + private static string ComputeDefiningProjectFileName(string itemSpec) + => ComputeFileName(itemSpec); + + private static string ComputeDefiningProjectExtension(string itemSpec) + => ComputeExtension(itemSpec); + + /// + /// Indicates whether the given path is a UNC or drive pattern root directory. + /// Note: This function mimics the behavior of checking if Path.GetDirectoryName(path) == null. + /// + /// + /// + private static bool IsRootDirectory(string path) + { + // Eliminate all non-rooted paths + if (!Path.IsPathRooted(path)) + { + return false; + } + + int uncMatchLength = StartsWithUncPatternMatchLength(path); + + // Determine if the given path is a standard drive/unc pattern root + if (IsDrivePattern(path) || + IsDrivePatternWithSlash(path) || + uncMatchLength == path.Length) + { + return true; + } + + // Eliminate all non-root unc paths. + if (uncMatchLength != -1) + { + return false; + } + + // Eliminate any drive patterns that don't have a slash after the colon or where the 4th character is a non-slash + // A non-slash at [3] is specifically checked here because Path.GetDirectoryName + // considers "C:///" a valid root. + if (StartsWithDrivePattern(path) && + ((path.Length >= 3 && path[2] is not (BackSlash or ForwardSlash)) || + (path.Length >= 4 && path[3] is not (BackSlash or ForwardSlash)))) + { + return false; + } + + // There are some edge cases that can get to this point. + // After eliminating valid / invalid roots, fall back on original behavior. + return Path.GetDirectoryName(path) == null; + } + + /// + /// Temporary check for something like http://foo which will end up like c:\foo\bar\http://foo + /// We should either have no colon, or exactly one colon. + /// UNDONE: This is a minimal safe change for Dev10. The correct fix should be to make GetFullPath/NormalizePath throw for this. + /// + private static void ThrowForUrl(string fullPath, string itemSpec, string currentDirectory) + { + if (fullPath.IndexOf(':') != fullPath.LastIndexOf(':')) + { + // Cause a better error to appear + _ = Path.GetFullPath(Path.Combine(currentDirectory, itemSpec)); + } + } + } +} diff --git a/src/MSBuildTaskHost/Utilities/FileUtilities.cs b/src/MSBuildTaskHost/Utilities/FileUtilities.cs new file mode 100644 index 00000000000..3392272a243 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/FileUtilities.cs @@ -0,0 +1,304 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.TaskHost.Resources; + +namespace Microsoft.Build.TaskHost.Utilities; + +/// +/// This class contains utility methods for file IO. +/// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in +/// each class get pulled into the resulting assembly. +/// +internal static partial class FileUtilities +{ + private const char BackSlash = '\\'; + private const char ForwardSlash = '/'; + + // ISO 8601 Universal time with sortable format + private const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff"; + + public static string TempFileDirectory => Path.GetTempPath(); + + public static string MSBuildTaskHostDirectory + => field ??= Path.GetDirectoryName(Path.GetFullPath(typeof(FileUtilities).Assembly.Location)); + + /// + /// Indicates if the given character is a slash. + /// + /// + /// true, if slash + private static bool IsAnySlash(char c) + => c is BackSlash or ForwardSlash; + + /// + /// Indicates if the given file-spec ends with a slash. + /// + /// The file spec. + /// true, if file-spec has trailing slash + private static bool EndsWithSlash(string fileSpec) + => fileSpec.Length > 0 && IsAnySlash(fileSpec[fileSpec.Length - 1]); + + /// + /// Indicates whether the specified string follows the pattern drive pattern (for example "C:", "D:"). + /// + /// Input to check for drive pattern. + /// true if follows the drive pattern, false otherwise. + private static bool IsDrivePattern(string pattern) + => pattern.Length == 2 && StartsWithDrivePattern(pattern); // Format must be two characters long: ":" + + /// + /// Indicates whether the specified string follows the pattern drive pattern (for example "C:/" or "C:\"). + /// + /// Input to check for drive pattern with slash. + /// true if follows the drive pattern with slash, false otherwise. + private static bool IsDrivePatternWithSlash(string pattern) + => pattern.Length == 3 && StartsWithDrivePatternWithSlash(pattern); + + /// + /// Indicates whether the specified string starts with the drive pattern (for example "C:"). + /// + /// Input to check for drive pattern. + /// true if starts with drive pattern, false otherwise. + private static bool StartsWithDrivePattern(string pattern) + // Format dictates a length of at least 2, + // first character must be a letter, + // second character must be a ":" + => pattern.Length >= 2 && + (pattern[0] is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z')) && + pattern[1] == ':'; + + /// + /// Indicates whether the specified string starts with the drive pattern (for example "C:/" or "C:\"). + /// + /// Input to check for drive pattern. + /// true if starts with drive pattern with slash, false otherwise. + private static bool StartsWithDrivePatternWithSlash(string pattern) + // Format dictates a length of at least 3, + // first character must be a letter, + // second character must be a ":" + // third character must be a slash. + => pattern.Length >= 3 && + StartsWithDrivePattern(pattern) && + pattern[2] is BackSlash or ForwardSlash; + + /// + /// Indicates whether the specified file-spec comprises exactly "\\server\share" (with no trailing characters). + /// + /// Input to check for UNC pattern. + /// true if comprises UNC pattern. + private static bool IsUncPattern(string pattern) + // Return value == pattern.length means: + // meets minimum unc requirements + // pattern does not end in a '/' or '\' + // if a subfolder were found the value returned would be length up to that subfolder, therefore no subfolder exists + => StartsWithUncPatternMatchLength(pattern) == pattern.Length; + + /// + /// Indicates whether the specified file-spec begins with "\\server\share". + /// + /// Input to check for UNC pattern. + /// true if starts with UNC pattern. + private static bool StartsWithUncPattern(string pattern) + // Any non -1 value returned means there was a match, therefore is begins with the pattern. + => StartsWithUncPatternMatchLength(pattern) != -1; + + /// + /// Indicates whether the file-spec begins with a UNC pattern and how long the match is. + /// + /// Input to check for UNC pattern. + /// length of the match, -1 if no match. + private static int StartsWithUncPatternMatchLength(string pattern) + { + if (!MeetsUncPatternMinimumRequirements(pattern)) + { + return -1; + } + + bool prevCharWasSlash = true; + bool hasShare = false; + + for (int i = 2; i < pattern.Length; i++) + { + // Real UNC paths should only contain backslashes. However, the previous + // regex pattern accepted both so functionality will be retained. + if (pattern[i] is BackSlash or ForwardSlash) + { + if (prevCharWasSlash) + { + // We get here in the case of an extra slash. + return -1; + } + else if (hasShare) + { + return i; + } + + hasShare = true; + prevCharWasSlash = true; + } + else + { + prevCharWasSlash = false; + } + } + + if (!hasShare) + { + // no subfolder means no unc pattern. string is something like "\\abc" in this case + return -1; + } + + return pattern.Length; + } + + /// + /// Indicates whether or not the file-spec meets the minimum requirements of a UNC pattern. + /// + /// Input to check for UNC pattern minimum requirements. + /// true if the UNC pattern is a minimum length of 5 and the first two characters are be a slash, false otherwise. + private static bool MeetsUncPatternMinimumRequirements(string pattern) + => pattern.Length >= 5 && + pattern[0] is BackSlash or ForwardSlash && + pattern[1] is BackSlash or ForwardSlash; + + /// + /// Gets the canonicalized full path of the provided path. + /// Guidance for use: call this on all paths accepted through public entry + /// points that need normalization. After that point, only verify the path + /// is rooted, using ErrorUtilities.VerifyThrowPathRooted. + /// ASSUMES INPUT IS ALREADY UNESCAPED. + /// + private static string NormalizePath(string path) + { + ErrorUtilities.VerifyThrowArgumentLength(path); + string uncheckedFullPath = NativeMethods.GetFullPath(path); + + if (IsPathTooLong(uncheckedFullPath)) + { + string message = string.Format(SR.Shared_PathTooLong, path, NativeMethods.MaxPath); + throw new PathTooLongException(message); + } + + // We really don't care about extensions here, but Path.HasExtension provides a great way to + // invoke the CLR's invalid path checks (these are independent of path length) + _ = Path.HasExtension(uncheckedFullPath); + + // If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting + // and security checks for strings like \\?\GlobalRoot + return IsUNCPath(uncheckedFullPath) ? Path.GetFullPath(uncheckedFullPath) : uncheckedFullPath; + } + + private static bool IsUNCPath(string path) + { + if (!path.StartsWith(@"\\", StringComparison.Ordinal)) + { + return false; + } + + bool isUNC = true; + for (int i = 2; i < path.Length - 1; i++) + { + if (path[i] == '\\') + { + isUNC = false; + break; + } + } + + /* + From Path.cs in the CLR + + Throw an ArgumentException for paths like \\, \\server, \\server\ + This check can only be properly done after normalizing, so + \\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\ + (an internal kernel path) because it provides aliases for drives. + + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC")); + + // Check for \\?\Globalroot, an internal mechanism to the kernel + // that provides aliases for drives and other undocumented stuff. + // The kernel team won't even describe the full set of what + // is available here - we don't want managed apps mucking + // with this for security reasons. + */ + return isUNC || path.IndexOf(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase) != -1; + } + + /// + /// Extracts the directory from the given file-spec. + /// + /// The filespec. + /// directory path + private static string GetDirectory(string fileSpec) + { + string directory = Path.GetDirectoryName(fileSpec); + + // if file-spec is a root directory e.g. c:, c:\, \, \\server\share + // NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server + if (directory == null) + { + // just use the file-spec as-is + directory = fileSpec; + } + + if (directory.Length > 0 && !EndsWithSlash(directory)) + { + // restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories) + directory += Path.DirectorySeparatorChar; + } + + return directory; + } + + /// + /// Determines the full path for the given file-spec. + /// ASSUMES INPUT IS STILL ESCAPED. + /// + /// The file spec to get the full path of. + /// + /// Whether to escape the path after getting the full path. + /// Full path to the file, escaped if not specified otherwise. + private static string GetFullPath(string fileSpec, string currentDirectory, bool escape = true) + { + // Sending data out of the engine into the filesystem, so time to unescape. + fileSpec = EscapingUtilities.UnescapeAll(fileSpec); + + string fullPath = NormalizePath(Path.Combine(currentDirectory, fileSpec)); + + // In some cases we might want to NOT escape in order to preserve symbols like @, %, $ etc. + if (escape) + { + // Data coming back from the filesystem into the engine, so time to escape it back. + fullPath = EscapingUtilities.Escape(fullPath); + } + + if (!EndsWithSlash(fullPath) && (IsDrivePattern(fileSpec) || IsUncPattern(fullPath))) + { + // append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares) + fullPath += Path.DirectorySeparatorChar; + } + + return fullPath; + } + + private static bool IsPathTooLong(string path) + => path.Length >= NativeMethods.MaxPath; // >= not > because MAX_PATH assumes a trailing null + + internal static StreamWriter CreateWriterForAppend(string path) + { + const int DefaultFileStreamBufferSize = 4096; + + var fileStream = new FileStream( + path, + mode: FileMode.Append, + access: FileAccess.Write, + share: FileShare.Read, + bufferSize: DefaultFileStreamBufferSize, + options: FileOptions.SequentialScan); + + return new StreamWriter(fileStream); + } +} diff --git a/src/MSBuildTaskHost/Utilities/NativeMethods.cs b/src/MSBuildTaskHost/Utilities/NativeMethods.cs new file mode 100644 index 00000000000..c0f33d48d84 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/NativeMethods.cs @@ -0,0 +1,279 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace Microsoft.Build.TaskHost.Utilities; + +internal static class NativeMethods +{ + public static bool Is64Bit => IntPtr.Size == 8; + + private const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + + /// + /// Default buffer size to use when dealing with the Windows API. + /// + private const int MAX_PATH = 260; + + private const string WindowsFileSystemKeyName = @"SYSTEM\CurrentControlSet\Control\FileSystem"; + private const string LongPathsEnabledValueName = "LongPathsEnabled"; + + private enum LOGICAL_PROCESSOR_RELATIONSHIP + { + RelationProcessorCore, + RelationNumaNode, + RelationCache, + RelationProcessorPackage, + RelationGroup, + RelationAll = 0xffff + } + + private struct SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX + { + public LOGICAL_PROCESSOR_RELATIONSHIP Relationship; + public uint Size; + public PROCESSOR_RELATIONSHIP Processor; + + public SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX() + { + Relationship = default; + Size = default; + Processor = default; + } + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct PROCESSOR_RELATIONSHIP + { + public byte Flags; + private byte EfficiencyClass; + private fixed byte Reserved[20]; + public ushort GroupCount; + public IntPtr GroupInfo; + } + + /// + /// Contains information about a file or directory; used by GetFileAttributesEx. + /// + [StructLayout(LayoutKind.Sequential)] + private struct WIN32_FILE_ATTRIBUTE_DATA + { + internal int fileAttributes; + internal uint ftCreationTimeLow; + internal uint ftCreationTimeHigh; + internal uint ftLastAccessTimeLow; + internal uint ftLastAccessTimeHigh; + internal uint ftLastWriteTimeLow; + internal uint ftLastWriteTimeHigh; + internal uint fileSizeHigh; + internal uint fileSizeLow; + } + + public static int GetLogicalCoreCount() + { + int numberOfCpus = Environment.ProcessorCount; + + // .NET on Windows returns a core count limited to the current NUMA node + // https://github.com/dotnet/runtime/issues/29686 + // so always double-check it. + var result = GetLogicalCoreCountOnWindows(); + if (result != -1) + { + numberOfCpus = result; + } + + return numberOfCpus; + } + + /// + /// Get the exact physical core count on Windows + /// Useful for getting the exact core count in 32 bits processes, + /// as Environment.ProcessorCount has a 32-core limit in that case. + /// https://github.com/dotnet/runtime/blob/221ad5b728f93489655df290c1ea52956ad8f51c/src/libraries/System.Runtime.Extensions/src/System/Environment.Windows.cs#L171-L210 + /// + private static unsafe int GetLogicalCoreCountOnWindows() + { + uint len = 0; + const int ERROR_INSUFFICIENT_BUFFER = 122; + + if (!GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, IntPtr.Zero, ref len) && + Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER) + { + // Allocate that much space + var buffer = new byte[len]; + fixed (byte* bufferPtr = buffer) + { + // Call GetLogicalProcessorInformationEx with the allocated buffer + if (GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, (IntPtr)bufferPtr, ref len)) + { + // Walk each SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX in the buffer, where the Size of each dictates how + // much space it's consuming. For each group relation, count the number of active processors in each of its group infos. + int processorCount = 0; + byte* ptr = bufferPtr; + byte* endPtr = bufferPtr + len; + while (ptr < endPtr) + { + var current = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)ptr; + if (current->Relationship == LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore) + { + // Flags is 0 if the core has a single logical proc, LTP_PC_SMT if more than one + // for now, assume "more than 1" == 2, as it has historically been for hyperthreading + processorCount += (current->Processor.Flags == 0) ? 1 : 2; + } + + ptr += current->Size; + } + + return processorCount; + } + } + } + + return -1; + } + + /// + /// Cached value for MaxPath. + /// + private static int? s_maxPath; + + /// + /// Gets the max path limit of the current OS. + /// + internal static int MaxPath + => s_maxPath ??= ComputeMaxPath(); + + private static int ComputeMaxPath() + => Traits.Instance.EscapeHatches.DisableLongPaths || !LongPathsEnabled() + ? MAX_PATH + : int.MaxValue; + + private static bool LongPathsEnabled() + { + try + { + using RegistryKey? fileSystemKey = Registry.LocalMachine.OpenSubKey(WindowsFileSystemKeyName); + + if (fileSystemKey is null) + { + return false; + } + + int longPathsEnabledValue = (int)fileSystemKey.GetValue(LongPathsEnabledValueName, defaultValue: -1); + + return longPathsEnabledValue == 1; + } + catch + { + return false; + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP RelationshipType, IntPtr Buffer, ref uint ReturnedLength); + + /// + /// Given an error code, converts it to an HRESULT and throws the appropriate exception. + /// + /// + private static void ThrowExceptionForErrorCode(int errorCode) + { + // See ndp\clr\src\bcl\system\io\__error.cs for this code as it appears in the CLR. + + // Something really bad went wrong with the call + // translate the error into an exception + + // Convert the errorcode into an HRESULT (See MakeHRFromErrorCode in Win32Native.cs in + // ndp\clr\src\bcl\microsoft\win32) + errorCode = unchecked(((int)0x80070000) | errorCode); + + // Throw an exception as best we can + Marshal.ThrowExceptionForHR(errorCode); + } + + internal static unsafe string GetFullPath(string path) + { + char* buffer = stackalloc char[MAX_PATH]; + int fullPathLength = GetFullPathWin32(path, MAX_PATH, buffer, IntPtr.Zero); + + // if user is using long paths we could need to allocate a larger buffer + if (fullPathLength > MAX_PATH) + { + char* newBuffer = stackalloc char[fullPathLength]; + fullPathLength = GetFullPathWin32(path, fullPathLength, newBuffer, IntPtr.Zero); + + buffer = newBuffer; + } + + // Avoid creating new strings unnecessarily + return AreStringsEqual(buffer, fullPathLength, path) ? path : new string(buffer, startIndex: 0, length: fullPathLength); + } + + private static unsafe int GetFullPathWin32(string target, int bufferLength, char* buffer, IntPtr mustBeZero) + { + int pathLength = GetFullPathName(target, bufferLength, buffer, mustBeZero); + VerifyThrowWin32Result(pathLength); + return pathLength; + } + + /// + /// Compare an unsafe char buffer with a to see if their contents are identical. + /// + /// The beginning of the char buffer. + /// The length of the buffer. + /// The string. + /// True only if the contents of and the first characters in are identical. + private static unsafe bool AreStringsEqual(char* buffer, int len, string s) + { + if (len != s.Length) + { + return false; + } + + foreach (char ch in s) + { + if (ch != *buffer++) + { + return false; + } + } + + return true; + } + + private static void VerifyThrowWin32Result(int result) + { + bool isError = result == 0; + if (isError) + { + int code = Marshal.GetLastWin32Error(); + ThrowExceptionForErrorCode(code); + } + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); + + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetCurrentDirectory")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetCurrentDirectoryWindows(string path); + + internal static bool SetCurrentDirectory(string path) + => SetCurrentDirectoryWindows(path); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern unsafe int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero); + + public static bool FileExists(string fullPath) + { + WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); + bool success = GetFileAttributesEx(fullPath, 0, ref data); + return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0; + } +} diff --git a/src/MSBuildTaskHost/Utilities/StringBuilderCache.cs b/src/MSBuildTaskHost/Utilities/StringBuilderCache.cs new file mode 100644 index 00000000000..970d132fe44 --- /dev/null +++ b/src/MSBuildTaskHost/Utilities/StringBuilderCache.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Build.TaskHost.Utilities; + +/// +/// A cached reusable instance of StringBuilder. +/// +/// +/// An optimization that reduces the number of instances of constructed and collected. +/// +internal static class StringBuilderCache +{ + // The value 512 was chosen empirically as 95% percentile of returning string length. + private const int MAX_BUILDER_SIZE = 512; + + [ThreadStatic] + private static StringBuilder? t_cachedInstance; + + /// + /// Get a of at least the specified capacity. + /// + /// The suggested starting size of this instance. + /// A that may or may not be reused. + /// + /// It can be called any number of times; if a is in the cache then + /// it will be returned and the cache emptied. Subsequent calls will return a new . + /// + /// The instance is cached in Thread Local Storage and so there is one per thread. + /// + public static StringBuilder Acquire(int capacity = 16 /*StringBuilder.DefaultCapacity*/) + { + if (capacity <= MAX_BUILDER_SIZE) + { + StringBuilder? sb = t_cachedInstance; + t_cachedInstance = null; + if (sb != null) + { + // Avoid StringBuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + sb.Length = 0; // Equivalent of sb.Clear() that works on .Net 3.5 + return sb; + } + } + } + + return new StringBuilder(capacity); + } + + /// + /// Place the specified builder in the cache if it is not too big. Unbalanced Releases are acceptable. + /// The StringBuilder should not be used after it has + /// been released. + /// Unbalanced Releases are perfectly acceptable.It + /// will merely cause the runtime to create a new + /// StringBuilder next time Acquire is called. + /// + /// The to cache. Likely returned from . + /// + /// The StringBuilder should not be used after it has been released. + /// + /// + /// Unbalanced Releases are perfectly acceptable.It + /// will merely cause the runtime to create a new + /// StringBuilder next time Acquire is called. + /// + /// + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= MAX_BUILDER_SIZE) + { + // Assert we are not replacing another string builder. That could happen when Acquire is reentered. + // User of StringBuilderCache has to make sure that calling method call stacks do not also use StringBuilderCache. + Debug.Assert(t_cachedInstance == null, "Unexpected replacing of other StringBuilder."); + t_cachedInstance = sb; + } + } + + /// + /// Get a string and return its builder to the cache. + /// + /// Builder to cache (if it's not too big). + /// The equivalent to 's contents. + /// + /// Convenience method equivalent to calling followed by . + /// + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } +} diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index 119464d22dd..a80c985bff0 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -223,21 +223,29 @@ internal class Handshake protected readonly HandshakeComponents _handshakeComponents; /// - /// Initializes a new instance of the class with the specified node type - /// and optional predefined tools directory. + /// Initializes a new instance of the class with the specified node type. /// /// - /// The that specifies the type of node and configuration options for the handshake operation. + /// The that specifies the type of node and configuration options for the handshake operation. /// - /// - /// An optional directory path used for .NET TaskHost handshake salt calculation (only on .NET Framework). - /// When specified for .NET TaskHost nodes, this directory path is included in the handshake salt - /// to ensure the child dotnet process connects with the expected tools directory context. - /// For non-.NET TaskHost nodes or on .NET Core, the MSBuildToolsDirectoryRoot is used instead. - /// This parameter is ignored when not running .NET TaskHost on .NET Framework. + public Handshake(HandshakeOptions nodeType) + : this(nodeType, includeSessionId: true, toolsDirectory: null) + { + } + + /// + /// Initializes a new instance of the class with the specified node type + /// and optional predefined tools directory. + /// + /// + /// The that specifies the type of node and configuration options for the handshake operation. /// - internal Handshake(HandshakeOptions nodeType, string predefinedToolsDirectory = null) - : this(nodeType, includeSessionId: true, predefinedToolsDirectory) + /// + /// The directory path to use for handshake salt calculation. For some task hosts, notably the .NET TaskHost (on .NET Framework) + /// and the CLR2 TaskHost, this is needed to ensure the child process connects with the expected tools directory context. + /// + public Handshake(HandshakeOptions nodeType, string toolsDirectory) + : this(nodeType, includeSessionId: true, toolsDirectory) { } @@ -247,19 +255,30 @@ internal Handshake(HandshakeOptions nodeType, string predefinedToolsDirectory = // Source options of the handshake. internal HandshakeOptions HandshakeOptions { get; } - protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string predefinedToolsDirectory) + protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string toolsDirectory) { HandshakeOptions = nodeType; +#if NETFRAMEWORK + ErrorUtilities.VerifyThrow( + toolsDirectory is null || IsNetTaskHost || IsClr2TaskHost, + $"{toolsDirectory} should only be provided for .NET or CLR2 TaskHost nodes (and only when running on .NET Framework)."); +#else + ErrorUtilities.VerifyThrow( + toolsDirectory is null, + $"{toolsDirectory} should not have been provided."); +#endif + + toolsDirectory ??= BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot; + // Build handshake options with version in upper bits const int handshakeVersion = (int)CommunicationsUtilities.handshakeVersion; var options = (int)nodeType | (handshakeVersion << 24); CommunicationsUtilities.Trace("Building handshake for node type {0}, (version {1}): options {2}.", nodeType, handshakeVersion, options); // Calculate salt from environment and tools directory - bool isNetTaskHost = IsHandshakeOptionEnabled(nodeType, HandshakeOptions.NET | HandshakeOptions.TaskHost); string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? ""; - string toolsDirectory = GetToolsDirectory(isNetTaskHost, predefinedToolsDirectory); + int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}"); CommunicationsUtilities.Trace("Handshake salt is {0}", handshakeSalt); @@ -273,20 +292,17 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string pre sessionId = currentProcess.SessionId; } - _handshakeComponents = isNetTaskHost + _handshakeComponents = IsNetTaskHost ? CreateNetTaskHostComponents(options, salt, sessionId) : CreateStandardComponents(options, salt, sessionId); } - private string GetToolsDirectory(bool isNetTaskHost, string predefinedToolsDirectory) => -#if NETFRAMEWORK - isNetTaskHost + private bool IsNetTaskHost + => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.NET | HandshakeOptions.TaskHost); - // For .NET TaskHost assembly directory we set the expectation for the child dotnet process to connect to. - ? predefinedToolsDirectory - : BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot; -#else - BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot; +#if NETFRAMEWORK + private bool IsClr2TaskHost + => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.CLR2 | HandshakeOptions.TaskHost); #endif private static HandshakeComponents CreateNetTaskHostComponents(int options, int salt, int sessionId) => new( @@ -336,7 +352,7 @@ internal sealed class ServerNodeHandshake : Handshake public override byte? ExpectedVersionInFirstByte => null; internal ServerNodeHandshake(HandshakeOptions nodeType) - : base(nodeType, includeSessionId: false, predefinedToolsDirectory: null) + : base(nodeType, includeSessionId: false, toolsDirectory: null) { } diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index 56a9f856af8..6459220a2a6 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -15,6 +15,12 @@ namespace Microsoft.Build.BackEnd /// Enumeration of all of the packet types used for communication. /// Uses lower 6 bits for packet type (0-63), upper 2 bits reserved for flags. /// + /// + /// Several of these values must be kept in sync with MSBuildTaskHost's NodePacketType. + /// The values shared with MSBuildTaskHost are , + /// , , , + /// , and . + /// internal enum NodePacketType : byte { // Mask for extracting packet type (lower 6 bits) diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 33e7c619c97..57c7ffe3c46 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -23,6 +23,9 @@ namespace Microsoft.Build.Shared /// An enumeration of all the types of BuildEventArgs that can be /// packaged by this logMessagePacket /// + /// + /// Several of these values must be kept in sync with MSBuildTaskHost's LoggingEventType. + /// internal enum LoggingEventType : int { /// diff --git a/src/Shared/UnitTests/ImmutableDictionary_Tests.cs b/src/Shared/UnitTests/ImmutableDictionary_Tests.cs deleted file mode 100644 index e0ecf9bc051..00000000000 --- a/src/Shared/UnitTests/ImmutableDictionary_Tests.cs +++ /dev/null @@ -1,260 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// We don't automatically run these tests against the BCL implementation of ImmutableDictionary as it would require dual-compiling -// this file. When making changes to this test, though, it is recommended to run them manually by uncommenting the following line. -// This helps ensure that the real thing has the same behavior that we expect in our implementation. -// #define _TEST_BCL_IMMUTABLE_DICTIONARY - -extern alias MSBuildTaskHost; - -using System; -using System.Collections; -using System.Collections.Generic; - -using Shouldly; -using Xunit; - -#if _TEST_BCL_IMMUTABLE_DICTIONARY -using ImmutableDictionary = System.Collections.Immutable.ImmutableDictionary; -#else -using ImmutableDictionary = MSBuildTaskHost::System.Collections.Immutable.ImmutableDictionary; -#endif - -#nullable disable - -namespace Microsoft.Build.UnitTests -{ - public class ImmutableDictionary_Tests - { - private readonly ImmutableDictionary _emptyDict = ImmutableDictionary.Empty; - - [Fact] - public void SimplesBoolPropertiesReturnExpectedValues() - { - ((IDictionary)_emptyDict).IsFixedSize.ShouldBeTrue(); - ((IDictionary)_emptyDict).IsReadOnly.ShouldBeTrue(); - ((IDictionary)_emptyDict).IsSynchronized.ShouldBeTrue(); - } - - [Fact] - public void CountReturnsExpectedValue() - { - _emptyDict.Count.ShouldBe(0); - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - dict.Count.ShouldBe(1); - dict = dict.SetItem("Key2", "Value2"); - dict.Count.ShouldBe(2); - dict = dict.Clear(); - dict.Count.ShouldBe(0); - } - - [Fact] - public void IndexerReturnsPreviouslySetItem() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - dict["Key1"].ShouldBe("Value1"); - ((IDictionary)dict)["Key1"].ShouldBe("Value1"); - ((IDictionary)dict)["Key1"].ShouldBe("Value1"); - } - - [Fact] - public void IndexerThrowsForItemNotPreviouslySet() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - Should.Throw(() => _ = dict["Key2"]); - Should.Throw(() => _ = ((IDictionary)dict)["Key2"]); - Should.Throw(() => _ = ((IDictionary)dict)["Key2"]); - } - - [Fact] - public void ContainsReturnsTrueForPeviouslySetItem() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - dict.Contains(new KeyValuePair("Key1", "Value1")).ShouldBeTrue(); - dict.ContainsKey("Key1").ShouldBeTrue(); - ((IDictionary)dict).Contains("Key1").ShouldBeTrue(); - } - - [Fact] - public void ContainsReturnsFalseForItemNotPeviouslySet() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - dict.Contains(new KeyValuePair("Key2", "Value2")).ShouldBeFalse(); - dict.ContainsKey("Key2").ShouldBeFalse(); - ((IDictionary)dict).Contains("Key2").ShouldBeFalse(); - } - - [Fact] - public void EnumeratorEnumeratesItems() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - - IEnumerator> enumerator1 = dict.GetEnumerator(); - int i = 0; - while (enumerator1.MoveNext()) - { - i++; - enumerator1.Current.Key.ShouldBe("Key1"); - enumerator1.Current.Value.ShouldBe("Value1"); - } - i.ShouldBe(dict.Count); - - IDictionaryEnumerator enumerator2 = ((IDictionary)dict).GetEnumerator(); - i = 0; - while (enumerator2.MoveNext()) - { - i++; - enumerator2.Key.ShouldBe("Key1"); - enumerator2.Value.ShouldBe("Value1"); - } - i.ShouldBe(dict.Count); - - IEnumerator enumerator3 = ((IEnumerable)dict).GetEnumerator(); - i = 0; - while (enumerator3.MoveNext()) - { - i++; - KeyValuePair entry = (KeyValuePair)enumerator3.Current; - entry.Key.ShouldBe("Key1"); - entry.Value.ShouldBe("Value1"); - } - i.ShouldBe(dict.Count); - } - - [Fact] - public void CopyToCopiesItemsToArray() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - - KeyValuePair[] array1 = new KeyValuePair[1]; - ((ICollection>)dict).CopyTo(array1, 0); - array1[0].Key.ShouldBe("Key1"); - array1[0].Value.ShouldBe("Value1"); - - array1 = new KeyValuePair[2]; - ((ICollection>)dict).CopyTo(array1, 1); - array1[1].Key.ShouldBe("Key1"); - array1[1].Value.ShouldBe("Value1"); - - DictionaryEntry[] array2 = new DictionaryEntry[1]; - ((ICollection)dict).CopyTo(array2, 0); - array2[0].Key.ShouldBe("Key1"); - array2[0].Value.ShouldBe("Value1"); - - array2 = new DictionaryEntry[2]; - ((ICollection)dict).CopyTo(array2, 1); - array2[1].Key.ShouldBe("Key1"); - array2[1].Value.ShouldBe("Value1"); - } - - [Fact] - public void CopyToThrowsOnInvalidInput() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - - Should.Throw(() => ((ICollection>)dict).CopyTo(null, 0)); - Should.Throw(() => ((ICollection)dict).CopyTo(null, 0)); - - KeyValuePair[] array1 = new KeyValuePair[1]; - DictionaryEntry[] array2 = new DictionaryEntry[1]; - Should.Throw(() => ((ICollection>)dict).CopyTo(array1, -1)); - Should.Throw(() => ((ICollection)dict).CopyTo(array1, -1)); - - Should.Throw(() => ((ICollection>)dict).CopyTo(array1, 1)); - Should.Throw(() => ((ICollection)dict).CopyTo(array1, 1)); - } - - [Fact] - public void KeysReturnsKeys() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - - ICollection keys1 = ((IDictionary)dict).Keys; - keys1.ShouldBe(new string[] { "Key1" }); - - ICollection keys2 = ((IDictionary)dict).Keys; - keys2.ShouldBe(new string[] { "Key1" }); - } - - [Fact] - public void ValuesReturnsValues() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - - ICollection values1 = ((IDictionary)dict).Values; - values1.ShouldBe(new string[] { "Value1" }); - - ICollection values2 = ((IDictionary)dict).Values; - values2.ShouldBe(new string[] { "Value1" }); - } - - [Fact] - public void SetItemReturnsNewInstanceAfterAdding() - { - ImmutableDictionary dict = _emptyDict.SetItem("Key1", "Value1"); - dict.ShouldNotBeSameAs(_emptyDict); - } - - [Fact] - public void SetItemReturnsNewInstanceAfterUpdating() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.SetItem("Key1", "Value2"); - dict2.ShouldNotBeSameAs(dict1); - } - - [Fact] - public void SetItemReturnsSameInstanceWhenItemAlreadyExists() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.SetItem("Key1", "Value1"); - dict2.ShouldBeSameAs(dict1); - } - - [Fact] - public void RemoveReturnsNewInstanceAfterDeleting() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.Remove("Key1"); - dict2.ShouldNotBeSameAs(dict1); - } - - [Fact] - public void RemoveReturnsSameInstanceWhenItemDoesNotExist() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.Remove("Key2"); - dict2.ShouldBeSameAs(dict1); - } - - [Fact] - public void ClearReturnsNewInstance() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.Clear(); - dict2.ShouldNotBeSameAs(dict1); - } - - [Fact] - public void WithComparersCreatesNewInstanceWithSpecifiedKeyComparer() - { - ImmutableDictionary dict1 = _emptyDict.SetItem("Key1", "Value1"); - ImmutableDictionary dict2 = dict1.WithComparers(StringComparer.OrdinalIgnoreCase); - dict2["KEY1"].ShouldBe("Value1"); - } - - [Fact] - public void AddRangeAddsAllItems() - { - ImmutableDictionary dict = _emptyDict.AddRange(new KeyValuePair[] - { - new KeyValuePair("Key1", "Value1"), - new KeyValuePair("Key2", "Value2") - }); - dict.Count.ShouldBe(2); - dict["Key1"].ShouldBe("Value1"); - dict["Key2"].ShouldBe("Value2"); - } - } -} diff --git a/src/UnitTests.Shared/WindowsNet35OnlyFactAttribute.cs b/src/UnitTests.Shared/WindowsNet35OnlyFactAttribute.cs new file mode 100644 index 00000000000..625c5387df8 --- /dev/null +++ b/src/UnitTests.Shared/WindowsNet35OnlyFactAttribute.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Build.Shared; +using Xunit; + +namespace Microsoft.Build.UnitTests.Shared; + +public class WindowsNet35OnlyFactAttribute : FactAttribute +{ + private const string Message = "This test only runs on Windows under .NET Framework when .NET Framework 3.5 is installed."; + + public WindowsNet35OnlyFactAttribute(string? additionalMessage = null) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || + !RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase) || + !IsNetFramework35Installed() || + !BootstrapHasNetFxMicrosoftNetBuildExtensions()) + { + Skip = SkipMessage(additionalMessage); + } + } + + private static string SkipMessage(string? additionalMessage = null) + => !string.IsNullOrWhiteSpace(additionalMessage) ? $"{Message} {additionalMessage}" : Message; + + private static bool IsNetFramework35Installed() + => FrameworkLocationHelper.GetPathToDotNetFrameworkV35(DotNetFrameworkArchitecture.Current) != null; + + /// + /// Checks to see if the .NET Framework version of Microsoft.NET.Build.Extensions is installed. + /// If it isn't, building building for .NET Framework 3.5 will fail. + /// + private static bool BootstrapHasNetFxMicrosoftNetBuildExtensions() + { + var binDir = new DirectoryInfo(RunnerUtilities.BootstrapMsBuildBinaryLocation); + + // The bin directory should be something like, D:\repo\msbuild\artifacts\bin\bootstrap\net472\MSBuild\Current\Bin + // Walk up three levels to get the TFM folder name. + // Then, look for .\bootstrap\TFM\MSBuild\Microsoft\Microsoft.NET.Build.Extensions\tools\TFM + if (binDir == null || !"Bin".Equals(binDir.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var currentDir = binDir.Parent; + if (currentDir == null || !"Current".Equals(currentDir.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var msbuildDir = currentDir.Parent; + if (msbuildDir == null || !"MSBuild".Equals(msbuildDir.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var tfmDir = msbuildDir.Parent; + string? tfm = tfmDir?.Name; + + if (tfm == null || !tfm.StartsWith("net4", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var directories = msbuildDir.GetDirectories(@"Microsoft\Microsoft.NET.Build.Extensions\tools\net4*"); + + return Array.Exists(directories, x => x.Name == tfm); + } +}