Skip to content

Commit b481c50

Browse files
YuliiaKovalovaCopilotrainersigwald
authored
Fix task host launch regressions from apphost support (#13325)
## Summary Fixes three regressions introduced by #13175 (Add App Host Support for MSBuild, commit c8011cb). ## Bug 1: CLR2 task host (MSBuildTaskHost.exe) broken - VS build failures **Root cause**: `NodeLauncher.ResolveExecutableName` only recognized `MSBuild.exe` (`Constants.MSBuildExecutableName`) as a native executable. Any other `.exe` - including `MSBuildTaskHost.exe` - was treated as a managed assembly and routed through `dotnet.exe`. Since `MSBuildTaskHost.exe` is a standalone .NET Framework 3.5 executable, `dotnet.exe` cannot host it. ## Bug 2: .NET task host fallback broken when `DotnetHostPath` is null **Symptom**: `MSB4216: ... the required executable "...\sdk\11.0.100-ci\MSBuild.exe" exists and can be run` - in CI where the apphost hasn't been created yet. **Root cause**: `ResolveAppHostOrFallback` correctly detects the missing apphost and tries to fall back to `dotnet MSBuild.dll`, but `TaskHostParameters.DotnetHostPath` is null (not populated by `AssemblyTaskFactory`). This produces `NodeLaunchData(null, ...)` which silently fails in `CreateNode`. ## Bug 3: MSB4216 error shows wrong executable path for .NET task hosts **Root cause**: `LogErrorUnableToCreateTaskHost` in `TaskHostTask.cs` has a `#if NETFRAMEWORK` guard around the .NET task host path resolution. On .NET Core (the common case), it always falls through to `GetMSBuildExecutablePathForNonNETRuntimes` which returns the non-.NET path (`MSBuild.exe`), even for `.NET` task host failures. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rainer Sigwald <raines@microsoft.com>
1 parent 3477e49 commit b481c50

4 files changed

Lines changed: 131 additions & 15 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using Microsoft.Build.UnitTests;
6+
using Microsoft.Build.UnitTests.Shared;
7+
using Shouldly;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
11+
namespace Microsoft.Build.Engine.UnitTests;
12+
13+
/// <summary>
14+
/// End-to-end tests for the CLR2 task host (MSBuildTaskHost.exe).
15+
/// These tests explicitly force tasks to run out-of-proc in CLR2 via
16+
/// <c>TaskFactory="TaskHostFactory"</c> and <c>Runtime="CLR2"</c>,
17+
/// exercising the CLR2 branch in <c>ResolveNodeLaunchConfiguration</c>.
18+
/// </summary>
19+
public class CLR2TaskHost_E2E_Tests
20+
{
21+
private readonly ITestOutputHelper _output;
22+
23+
public CLR2TaskHost_E2E_Tests(ITestOutputHelper output)
24+
{
25+
_output = output;
26+
}
27+
28+
/// <summary>
29+
/// Verifies that the CLR2 task host (MSBuildTaskHost.exe) can be launched and connected to
30+
/// when a task explicitly requests Runtime="CLR2" with TaskHostFactory.
31+
///
32+
/// Regression test for the apphost changes (PR #13175) that replaced the three-branch
33+
/// ResolveNodeLaunchConfiguration with a two-branch version, losing the CLR2-specific path:
34+
/// 1. Empty command-line args (MSBuildTaskHost.Main() takes no arguments)
35+
/// 2. Handshake with toolsDirectory set to the EXE's directory so the pipe name
36+
/// salt matches what the child process computes on startup
37+
/// Without these, the parent and child compute different pipe name hashes → MSB4216.
38+
/// </summary>
39+
[WindowsNet35OnlyFact]
40+
public void ExplicitCLR2TaskHostFactory_RunsTaskSuccessfully()
41+
{
42+
using TestEnvironment env = TestEnvironment.Create(_output);
43+
TransientTestFolder testFolder = env.CreateFolder(createFolder: true);
44+
45+
string projectContent = """
46+
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
47+
<!-- Force Exec to run out-of-proc in the CLR2 task host (MSBuildTaskHost.exe) -->
48+
<UsingTask TaskName="Microsoft.Build.Tasks.Exec"
49+
AssemblyName="Microsoft.Build.Tasks.v3.5, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
50+
TaskFactory="TaskHostFactory"
51+
Runtime="CLR2" />
52+
53+
<Target Name="Build">
54+
<Exec Command="echo CLR2TaskHostSuccess" />
55+
</Target>
56+
</Project>
57+
""";
58+
59+
string projectPath = Path.Combine(testFolder.Path, "CLR2ExplicitTest.proj");
60+
File.WriteAllText(projectPath, projectContent);
61+
62+
string testOutput = RunnerUtilities.ExecBootstrapedMSBuild(
63+
$"\"{projectPath}\" -v:n",
64+
out bool success,
65+
outputHelper: _output);
66+
67+
68+
// MSB4216 occurs when the parent can't connect to MSBuildTaskHost.exe —
69+
// either due to handshake salt mismatch (missing toolsDirectory) or wrong process routing.
70+
testOutput.ShouldNotContain("MSB4216", customMessage: "CLR2 task host connection should succeed with correct handshake salt and empty command-line args");
71+
72+
success.ShouldBeTrue(customMessage: "Task explicitly requesting CLR2 + TaskHostFactory should execute in MSBuildTaskHost.exe");
73+
74+
// Verify the task actually ran by checking for its output.
75+
testOutput.ShouldContain("CLR2TaskHostSuccess", customMessage: "Exec task output should be visible, confirming it ran in CLR2 task host");
76+
}
77+
}

src/Build/BackEnd/Components/Communications/NodeLauncher.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,17 @@ private string ResolveExecutableName(string msbuildLocation, out bool isNativeAp
8383
isNativeAppHost = false;
8484

8585
#if RUNTIME_TYPE_NETCORE
86-
// If msbuildLocation is a native app host (e.g., MSBuild.exe on Windows, MSBuild on Linux), run it directly.
87-
// Otherwise, use dotnet.exe to run the managed assembly (e.g., MSBuild.dll).
8886
string fileName = Path.GetFileName(msbuildLocation);
89-
isNativeAppHost = fileName.Equals(Constants.MSBuildExecutableName, StringComparison.OrdinalIgnoreCase);
90-
if (!isNativeAppHost)
87+
88+
// Only managed assemblies (.dll) need dotnet.exe as a host.
89+
// All native executables — MSBuild app host, MSBuildTaskHost.exe, etc. — run directly.
90+
if (fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
9191
{
9292
return CurrentHost.GetCurrentHost();
9393
}
94+
95+
// Any .exe or extensionless binary (Linux app host) is a native executable.
96+
isNativeAppHost = true;
9497
#endif
9598
return msbuildLocation;
9699
}

src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -683,12 +683,30 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
683683
return nodeContexts.Count == 1;
684684

685685
// Resolves the node launch configuration based on the host context.
686-
NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters) =>
687-
686+
NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters)
687+
{
688688
// Handle .NET task host context
689-
Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET)
690-
? ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext))
691-
: new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
689+
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET))
690+
{
691+
return ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext));
692+
}
693+
694+
#if FEATURE_NET35_TASKHOST
695+
// CLR2 task host (MSBuildTaskHost.exe) requires special handling:
696+
// - Empty command-line args (MSBuildTaskHost.Main() takes no arguments)
697+
// - Handshake with toolsDirectory set to the EXE's directory so the
698+
// salt matches what the child process computes on startup.
699+
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2))
700+
{
701+
string msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext);
702+
string toolsDirectory = Path.GetDirectoryName(msbuildLocation) ?? string.Empty;
703+
return new NodeLaunchData(msbuildLocation, string.Empty, new Handshake(hostContext, toolsDirectory));
704+
}
705+
#endif
706+
707+
// CLR4 task host (MSBuild.exe on .NET Framework)
708+
return new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
709+
}
692710
}
693711

694712
/// <summary>
@@ -730,10 +748,19 @@ private NodeLaunchData ResolveAppHostOrFallback(
730748
dotnetOverrides);
731749
}
732750

733-
CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, dotnetHostPath);
751+
// Auto-discover dotnet host path when not explicitly provided.
752+
string resolvedDotnetHostPath = dotnetHostPath;
753+
#if RUNTIME_TYPE_NETCORE
754+
if (string.IsNullOrEmpty(resolvedDotnetHostPath))
755+
{
756+
resolvedDotnetHostPath = CurrentHost.GetCurrentHost();
757+
}
758+
#endif
759+
760+
CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, resolvedDotnetHostPath);
734761

735762
return new NodeLaunchData(
736-
dotnetHostPath,
763+
resolvedDotnetHostPath,
737764
$"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" {commandLineArgs}",
738765
new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath));
739766
}

src/Build/Instance/TaskFactories/TaskHostTask.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
77
using System.Diagnostics;
8+
using System.IO;
89
using System.Reflection;
910
using System.Threading;
1011
using Microsoft.Build.BackEnd.Logging;
@@ -13,6 +14,7 @@
1314
using Microsoft.Build.Framework;
1415
using Microsoft.Build.Internal;
1516
using Microsoft.Build.Shared;
17+
using Constants = Microsoft.Build.Framework.Constants;
1618
#if FEATURE_REPORTFILEACCESSES
1719
using Microsoft.Build.Experimental.FileAccess;
1820
using Microsoft.Build.FileAccesses;
@@ -700,13 +702,20 @@ private void HandleCoresRequest(TaskHostCoresRequest request)
700702
/// </summary>
701703
private void LogErrorUnableToCreateTaskHost(HandshakeOptions requiredContext, string runtime, string architecture, Exception e)
702704
{
703-
string taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildExecutablePathForNonNETRuntimes(requiredContext);
704-
#if NETFRAMEWORK
705+
string taskHostLocation;
706+
705707
if (Handshake.IsHandshakeOptionEnabled(requiredContext, HandshakeOptions.NET))
706708
{
707-
taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters).MSBuildPath;
709+
(_, string msbuildPath) = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters);
710+
taskHostLocation = msbuildPath != null
711+
? Path.Combine(msbuildPath, Constants.MSBuildExecutableName)
712+
: null;
708713
}
709-
#endif
714+
else
715+
{
716+
taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildExecutablePathForNonNETRuntimes(requiredContext);
717+
}
718+
710719
string msbuildLocation = taskHostLocation ??
711720
// We don't know the path -- probably we're trying to get a 64-bit assembly on a
712721
// 32-bit machine. At least give them the exe name to look for, though ...

0 commit comments

Comments
 (0)