Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/Build.UnitTests/CLR2TaskHost_E2E_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.Build.Engine.UnitTests;

/// <summary>
/// End-to-end tests for the CLR2 task host (MSBuildTaskHost.exe).
/// These tests explicitly force tasks to run out-of-proc in CLR2 via
/// <c>TaskFactory="TaskHostFactory"</c> and <c>Runtime="CLR2"</c>,
/// exercising the CLR2 branch in <c>ResolveNodeLaunchConfiguration</c>.
/// </summary>
public class CLR2TaskHost_E2E_Tests
{
private readonly ITestOutputHelper _output;

public CLR2TaskHost_E2E_Tests(ITestOutputHelper output)
{
_output = output;
}

/// <summary>
/// Verifies that the CLR2 task host (MSBuildTaskHost.exe) can be launched and connected to
/// when a task explicitly requests Runtime="CLR2" with TaskHostFactory.
///
/// Regression test for the apphost changes (PR #13175) that replaced the three-branch
/// ResolveNodeLaunchConfiguration with a two-branch version, losing the CLR2-specific path:
/// 1. Empty command-line args (MSBuildTaskHost.Main() takes no arguments)
/// 2. Handshake with toolsDirectory set to the EXE's directory so the pipe name
/// salt matches what the child process computes on startup
/// Without these, the parent and child compute different pipe name hashes → MSB4216.
/// </summary>
[WindowsNet35OnlyFact]
public void ExplicitCLR2TaskHostFactory_RunsTaskSuccessfully()
{
using TestEnvironment env = TestEnvironment.Create(_output);
TransientTestFolder testFolder = env.CreateFolder(createFolder: true);

string projectContent = """
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Force Exec to run out-of-proc in the CLR2 task host (MSBuildTaskHost.exe) -->
<UsingTask TaskName="Microsoft.Build.Tasks.Exec"
AssemblyName="Microsoft.Build.Tasks.v3.5, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
TaskFactory="TaskHostFactory"
Runtime="CLR2" />

<Target Name="Build">
<Exec Command="echo CLR2TaskHostSuccess" />
</Target>
</Project>
""";

string projectPath = Path.Combine(testFolder.Path, "CLR2ExplicitTest.proj");
File.WriteAllText(projectPath, projectContent);

string testOutput = RunnerUtilities.ExecBootstrapedMSBuild(
$"\"{projectPath}\" -v:n",
out bool success,
outputHelper: _output);


// MSB4216 occurs when the parent can't connect to MSBuildTaskHost.exe —
// either due to handshake salt mismatch (missing toolsDirectory) or wrong process routing.
testOutput.ShouldNotContain("MSB4216", customMessage: "CLR2 task host connection should succeed with correct handshake salt and empty command-line args");

success.ShouldBeTrue(customMessage: "Task explicitly requesting CLR2 + TaskHostFactory should execute in MSBuildTaskHost.exe");

// Verify the task actually ran by checking for its output.
testOutput.ShouldContain("CLR2TaskHostSuccess", customMessage: "Exec task output should be visible, confirming it ran in CLR2 task host");
}
}
11 changes: 7 additions & 4 deletions src/Build/BackEnd/Components/Communications/NodeLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,17 @@ private string ResolveExecutableName(string msbuildLocation, out bool isNativeAp
isNativeAppHost = false;

#if RUNTIME_TYPE_NETCORE
// If msbuildLocation is a native app host (e.g., MSBuild.exe on Windows, MSBuild on Linux), run it directly.
// Otherwise, use dotnet.exe to run the managed assembly (e.g., MSBuild.dll).
string fileName = Path.GetFileName(msbuildLocation);
isNativeAppHost = fileName.Equals(Constants.MSBuildExecutableName, StringComparison.OrdinalIgnoreCase);
if (!isNativeAppHost)

// Only managed assemblies (.dll) need dotnet.exe as a host.
// All native executables — MSBuild app host, MSBuildTaskHost.exe, etc. — run directly.
if (fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
return CurrentHost.GetCurrentHost();
}

// Any .exe or extensionless binary (Linux app host) is a native executable.
isNativeAppHost = true;
#endif
return msbuildLocation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,12 +683,30 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
return nodeContexts.Count == 1;

// Resolves the node launch configuration based on the host context.
NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters) =>

NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters)
{
// Handle .NET task host context
Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET)
? ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext))
: new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET))
{
return ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext));
}

#if FEATURE_NET35_TASKHOST
// CLR2 task host (MSBuildTaskHost.exe) requires special handling:
// - Empty command-line args (MSBuildTaskHost.Main() takes no arguments)
// - Handshake with toolsDirectory set to the EXE's directory so the
// salt matches what the child process computes on startup.
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2))
{
string msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext);
string toolsDirectory = Path.GetDirectoryName(msbuildLocation) ?? string.Empty;
return new NodeLaunchData(msbuildLocation, string.Empty, new Handshake(hostContext, toolsDirectory));
}
#endif

// CLR4 task host (MSBuild.exe on .NET Framework)
return new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
}
}

/// <summary>
Expand Down Expand Up @@ -730,10 +748,19 @@ private NodeLaunchData ResolveAppHostOrFallback(
dotnetOverrides);
}

CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, dotnetHostPath);
// Auto-discover dotnet host path when not explicitly provided.
string resolvedDotnetHostPath = dotnetHostPath;
#if RUNTIME_TYPE_NETCORE
if (string.IsNullOrEmpty(resolvedDotnetHostPath))
{
resolvedDotnetHostPath = CurrentHost.GetCurrentHost();
}
#endif

CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, resolvedDotnetHostPath);

return new NodeLaunchData(
dotnetHostPath,
resolvedDotnetHostPath,
$"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" {commandLineArgs}",
new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath));
}
Expand Down
17 changes: 13 additions & 4 deletions src/Build/Instance/TaskFactories/TaskHostTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using Microsoft.Build.BackEnd.Logging;
Expand All @@ -13,6 +14,7 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Constants = Microsoft.Build.Framework.Constants;
#if FEATURE_REPORTFILEACCESSES
using Microsoft.Build.Experimental.FileAccess;
using Microsoft.Build.FileAccesses;
Expand Down Expand Up @@ -700,13 +702,20 @@ private void HandleCoresRequest(TaskHostCoresRequest request)
/// </summary>
private void LogErrorUnableToCreateTaskHost(HandshakeOptions requiredContext, string runtime, string architecture, Exception e)
{
string taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildExecutablePathForNonNETRuntimes(requiredContext);
#if NETFRAMEWORK
string taskHostLocation;

if (Handshake.IsHandshakeOptionEnabled(requiredContext, HandshakeOptions.NET))
{
taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters).MSBuildPath;
(_, string msbuildPath) = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters);
taskHostLocation = msbuildPath != null
? Path.Combine(msbuildPath, Constants.MSBuildExecutableName)
: null;
}
#endif
else
{
taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildExecutablePathForNonNETRuntimes(requiredContext);
}

string msbuildLocation = taskHostLocation ??
// We don't know the path -- probably we're trying to get a 64-bit assembly on a
// 32-bit machine. At least give them the exe name to look for, though ...
Expand Down