From 6754350c1cda036326f53363df33e5c48c58cf42 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 30 Jan 2026 13:17:39 +0100 Subject: [PATCH 1/8] add hostservices translation support for clr 4 task host --- src/Framework/ITranslator.cs | 6 ++++++ src/Shared/TaskHostConfiguration.cs | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Framework/ITranslator.cs b/src/Framework/ITranslator.cs index 71059a49272..4cb69605be6 100644 --- a/src/Framework/ITranslator.cs +++ b/src/Framework/ITranslator.cs @@ -84,6 +84,12 @@ internal interface ITranslator : IDisposable /// 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. + /// Special values: + /// 0: Indicates Framework-to-Framework (e.g. TaskHost with CLR4 runtime) task host communication. + /// A value of 0 means version match is guaranteed since both processes are shipped together (version match validation on handshake). + /// 1: Base version for .NET task host communication without newer features. Skips serialization of AppDomain. + /// 2+: .NET task host communication with support for additional fields like HostServices, + /// TargetName, and ProjectFile in TaskHostConfiguration. /// byte NegotiatedPacketVersion { get; set; } diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index ddf1d1c1a5d..0ce8b664884 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -507,11 +507,14 @@ public void Translate(ITranslator translator) translator.Translate(ref _projectFileOfTask); translator.Translate(ref _taskName); translator.Translate(ref _taskLocation); - if (translator.NegotiatedPacketVersion >= 2) + + // Version 0 = Framework-to-Framework (e.g. CLR4), + // Version 2+ = .NET task host with support of translation for these fields. + if (translator.NegotiatedPacketVersion == 0 || translator.NegotiatedPacketVersion >= 2) { translator.Translate(ref _targetName); translator.Translate(ref _projectFile); -#if !NET35 +#if !NET35 translator.Translate(ref _hostServices); #endif } From ebae63add6177dba9b8c1fd2917afbddba2bedf7 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 30 Jan 2026 14:07:30 +0100 Subject: [PATCH 2/8] update the comment --- src/Framework/ITranslator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Framework/ITranslator.cs b/src/Framework/ITranslator.cs index 4cb69605be6..b559758a768 100644 --- a/src/Framework/ITranslator.cs +++ b/src/Framework/ITranslator.cs @@ -85,11 +85,9 @@ internal interface ITranslator : IDisposable /// The negotiated version is used to conditionally serialize/deserialize fields that may /// not be supported by older packet versions. /// Special values: - /// 0: Indicates Framework-to-Framework (e.g. TaskHost with CLR4 runtime) task host communication. - /// A value of 0 means version match is guaranteed since both processes are shipped together (version match validation on handshake). - /// 1: Base version for .NET task host communication without newer features. Skips serialization of AppDomain. - /// 2+: .NET task host communication with support for additional fields like HostServices, - /// TargetName, and ProjectFile in TaskHostConfiguration. + /// 0: Indicates Framework-to-Framework (e.g. TaskHost with CLR4 runtime) task host communication. + /// 1: Base version for .NET task host communication without newer features. Skips serialization of AppDomain. + /// 2+: NET task host communication with support for additional fields like HostServices, TargetName, and ProjectFile in TaskHostConfiguration. /// byte NegotiatedPacketVersion { get; set; } From 26e6a653809b0ecebbb2e3562f1b8272b730c134 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:19:23 +0100 Subject: [PATCH 3/8] exclude clr35 --- src/Shared/TaskHostConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index 0ce8b664884..a9fc26bb238 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -510,14 +510,14 @@ public void Translate(ITranslator translator) // Version 0 = Framework-to-Framework (e.g. CLR4), // Version 2+ = .NET task host with support of translation for these fields. +#if !NET35 if (translator.NegotiatedPacketVersion == 0 || translator.NegotiatedPacketVersion >= 2) { translator.Translate(ref _targetName); translator.Translate(ref _projectFile); -#if !NET35 translator.Translate(ref _hostServices); -#endif } +#endif translator.Translate(ref _isTaskInputLoggingEnabled); translator.TranslateDictionary(ref _taskParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); From 4b0ff33632ccbd8d465e1c215adf741584105ded Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 2 Feb 2026 10:55:04 +0100 Subject: [PATCH 4/8] update condition --- src/Shared/TaskHostConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index a9fc26bb238..d16ff439c6f 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -510,8 +510,8 @@ public void Translate(ITranslator translator) // Version 0 = Framework-to-Framework (e.g. CLR4), // Version 2+ = .NET task host with support of translation for these fields. -#if !NET35 - if (translator.NegotiatedPacketVersion == 0 || translator.NegotiatedPacketVersion >= 2) +#if NET472 || NETCOREAPP + if (translator.NegotiatedPacketVersion is 0 or >= 2) { translator.Translate(ref _targetName); translator.Translate(ref _projectFile); From 54e11d34b0e8d83e65042ae73a917d35bc5a4797 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 2 Feb 2026 12:00:02 +0100 Subject: [PATCH 5/8] more changes --- .../Communications/NodeProviderOutOfProcBase.cs | 6 ++++++ src/Framework/BinaryTranslator.cs | 4 ++-- src/Framework/ITranslator.cs | 8 ++++---- src/Shared/INodePacket.cs | 8 +++----- src/Shared/TaskHostConfiguration.cs | 6 +++--- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index c29d76b1c52..4047772c64e 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -849,6 +849,12 @@ private void DrainPacketQueue(object state) NodePacketTypeExtensions.WriteVersion(writeStream, context._negotiatedPacketVersion); writeTranslator.NegotiatedPacketVersion = context._negotiatedPacketVersion; } + else if (!Handshake.IsHandshakeOptionEnabled(_handshakeOptions, HandshakeOptions.CLR2)) + { + // CLR4 task hosts: set version to 0 to enable version-dependent fields. + // CLR2 task hosts: leave as null (default) to skip version-dependent fields. + writeTranslator.NegotiatedPacketVersion = 0; + } packet.Translate(writeTranslator); diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs index f25c79df899..684929529b0 100644 --- a/src/Framework/BinaryTranslator.cs +++ b/src/Framework/BinaryTranslator.cs @@ -119,7 +119,7 @@ public TranslationDirection Mode } /// - public byte NegotiatedPacketVersion { get; set; } + public byte? NegotiatedPacketVersion { get; set; } /// /// Translates a boolean. @@ -1006,7 +1006,7 @@ public TranslationDirection Mode } /// - public byte NegotiatedPacketVersion { get; set; } + public byte? NegotiatedPacketVersion { get; set; } /// /// Translates a boolean. diff --git a/src/Framework/ITranslator.cs b/src/Framework/ITranslator.cs index b559758a768..3b191f6503c 100644 --- a/src/Framework/ITranslator.cs +++ b/src/Framework/ITranslator.cs @@ -85,11 +85,11 @@ internal interface ITranslator : IDisposable /// The negotiated version is used to conditionally serialize/deserialize fields that may /// not be supported by older packet versions. /// Special values: - /// 0: Indicates Framework-to-Framework (e.g. TaskHost with CLR4 runtime) task host communication. - /// 1: Base version for .NET task host communication without newer features. Skips serialization of AppDomain. - /// 2+: NET task host communication with support for additional fields like HostServices, TargetName, and ProjectFile in TaskHostConfiguration. + /// null: CLR2 (NET35) task host communication. Version-dependent fields are skipped because NET35 doesn't have them. + /// 0: The constant value for Framework-to-Framework (CLR4) task host communication. Supports HostServices, TargetName, ProjectFile, and AppDomain. + /// 2+: .NET task host communication with full support for version-dependent fields. /// - byte NegotiatedPacketVersion { get; set; } + byte? NegotiatedPacketVersion { get; set; } /// /// Returns the current serialization mode. diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index af4502e57f0..690420ba214 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -291,11 +291,9 @@ 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. + /// null: CLR2 (NET35) task host. Version-dependent fields skipped (not compiled in NET35). + /// 0: The constant value for Framework-to-Framework (CLR4) task host. Supports HostServices, TargetName, ProjectFile. + /// 2+: .NET task host with full support for version-dependent fields. /// /// When incrementing this version, ensure compatibility with existing /// task hosts and update the corresponding deserialization logic. diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index d16ff439c6f..ee7d7550505 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -508,10 +508,10 @@ public void Translate(ITranslator translator) translator.Translate(ref _taskName); translator.Translate(ref _taskLocation); - // Version 0 = Framework-to-Framework (e.g. CLR4), - // Version 2+ = .NET task host with support of translation for these fields. + // null = CLR2 (NET35) task hosts which don't have these fields compiled in. + // 0 = CLR4, 2+ = .NET - both support these fields. #if NET472 || NETCOREAPP - if (translator.NegotiatedPacketVersion is 0 or >= 2) + if (translator.NegotiatedPacketVersion.HasValue && translator.NegotiatedPacketVersion is 0 or >= 2) { translator.Translate(ref _targetName); translator.Translate(ref _projectFile); From 881263c9fdfd887623a658154416a6a60385b62f Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 2 Feb 2026 12:51:52 +0100 Subject: [PATCH 6/8] fix the test --- .../BackEnd/TaskHostConfiguration_Tests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs index da40bef3702..8b77b14045c 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs @@ -463,8 +463,14 @@ public void TestTranslationWithAppDomainSetup(byte[] configBytes) setup.SetConfigurationBytes(configBytes); - ((ITranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); - INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + // Set version to 0 for CLR4 (Framework-to-Framework) communication which supports AppDomain. + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + writeTranslator.NegotiatedPacketVersion = 0; + ((ITranslatable)config).Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + readTranslator.NegotiatedPacketVersion = 0; + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(readTranslator); TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; From bc082b6976e861360d16e4b196de81695ceed8f0 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 2 Feb 2026 17:20:09 +0100 Subject: [PATCH 7/8] fix the issue --- src/Shared/NodeEndpointOutOfProcBase.cs | 4 +++- src/Shared/TaskHostConfiguration.cs | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 038c7949dbb..a5105a3d1b7 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -710,7 +710,7 @@ private void RunReadLoop( bool hasExtendedHeader = NodePacketTypeExtensions.HasExtendedHeader(rawType); NodePacketType packetType = hasExtendedHeader ? NodePacketTypeExtensions.GetNodePacketType(rawType) : (NodePacketType)rawType; - byte parentVersion = 0; + byte? parentVersion = null; if (hasExtendedHeader) { parentVersion = NodePacketTypeExtensions.ReadVersion(localReadPipe); @@ -721,6 +721,8 @@ private void RunReadLoop( ITranslator readTranslator = BinaryTranslator.GetReadTranslator(localReadPipe, _sharedReadBuffer); // parent sends a packet version that is already negotiated during handshake. + // null = no extended header (CLR2/CLR4 Framework task hosts) + // 2+ = extended header present (.NET task host) readTranslator.NegotiatedPacketVersion = parentVersion; _packetFactory.DeserializeAndRoutePacket(0, packetType, readTranslator); } diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index ee7d7550505..31732e30bba 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -480,10 +480,10 @@ public void Translate(ITranslator translator) translator.TranslateCulture(ref _uiCulture); #if FEATURE_APPDOMAIN // 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) + // null = CLR2 (NET35) task host - supports AppDomain + // 0 = CLR4 (NET472) task host - supports AppDomain + // We serialize AppDomain for Framework task hosts (null or 0), but not for .NET (>= 2). + if (translator.NegotiatedPacketVersion is null or 0) { byte[] appDomainConfigBytes = null; From 61aa130302f08e1c1b04a62823470107a025b43c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 2 Feb 2026 18:09:54 +0100 Subject: [PATCH 8/8] remove extra chane --- src/Shared/NodeEndpointOutOfProcBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index a5105a3d1b7..ae422e25e31 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -710,7 +710,7 @@ private void RunReadLoop( bool hasExtendedHeader = NodePacketTypeExtensions.HasExtendedHeader(rawType); NodePacketType packetType = hasExtendedHeader ? NodePacketTypeExtensions.GetNodePacketType(rawType) : (NodePacketType)rawType; - byte? parentVersion = null; + byte parentVersion = 0; if (hasExtendedHeader) { parentVersion = NodePacketTypeExtensions.ReadVersion(localReadPipe); @@ -721,8 +721,8 @@ private void RunReadLoop( ITranslator readTranslator = BinaryTranslator.GetReadTranslator(localReadPipe, _sharedReadBuffer); // parent sends a packet version that is already negotiated during handshake. - // null = no extended header (CLR2/CLR4 Framework task hosts) - // 2+ = extended header present (.NET task host) + // For Framework task hosts (CLR2/CLR4) without extended headers, defaults to 0. + // For .NET task hosts, read from extended header (>= 1). readTranslator.NegotiatedPacketVersion = parentVersion; _packetFactory.DeserializeAndRoutePacket(0, packetType, readTranslator); }