Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
144 changes: 144 additions & 0 deletions src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,150 @@ public void VerifySameFactoryCanGenerateDifferentTaskInstances()
}
}

/// <summary>
/// Verify that when task host factory is explicitly requested with Runtime=NET,
/// the task host does not use sidecar mode (short-lived, releases locks after build).
/// This is a regression test for https://github.com/dotnet/msbuild/issues/13013
/// </summary>
[Fact]
public void VerifyExplicitTaskHostFactoryWithRuntimeNetDoesNotUseSidecar()
Comment thread
ViktorHofer marked this conversation as resolved.
Outdated
{
ITask createdTask = null;
try
{
// Setup: Task host factory explicitly requested with Runtime=NET
TaskHostParameters factoryParameters = new(XMakeAttributes.MSBuildRuntimeValues.net);
SetupTaskFactory(factoryParameters, explicitlyLaunchTaskHost: true, isTaskHostFactory: true);

createdTask = _taskFactory.CreateTaskInstance(
ElementLocation.Create("MSBUILD"),
null,
new MockHost(),
TaskHostParameters.Empty,
#if FEATURE_APPDOMAIN
new AppDomainSetup(),
#endif
false,
scheduledNodeId: 1,
(string propName) => ProjectPropertyInstance.Create("test", "test"),
CreateStubTaskEnvironment());

createdTask.ShouldNotBeNull();
createdTask.ShouldBeOfType<TaskHostTask>();

// Verify that the task host is not using sidecar mode (short-lived)
// When TaskHostFactoryExplicitlyRequested is true, useSidecarTaskHost should be false
TaskHostTask taskHostTask = (TaskHostTask)createdTask;
bool useSidecarTaskHost = IsUsingSidecarMode(taskHostTask);
useSidecarTaskHost.ShouldBeFalse("When task host factory is explicitly requested, useSidecarTaskHost should be false to ensure short-lived task hosts that release locks");
}
finally
{
if (createdTask != null)
{
_taskFactory.CleanupTask(createdTask);
}
}
}

/// <summary>
/// Validates task host lifecycle behavior based on whether the TaskHost runtime matches
/// the executing MSBuild runtime and whether TaskHostFactory is explicitly requested.
///
/// Test scenarios:
/// 1. Runtime matches + TaskHostFactory requested → short-lived out of proc (no node reuse)
/// 2. Runtime matches + TaskHostFactory NOT requested → in-proc execution
/// 3. Runtime doesn't match + TaskHostFactory requested → short-lived out of proc (no node reuse)
/// 4. Runtime doesn't match + TaskHostFactory NOT requested → long-lived sidecar out of proc (node reuse enabled)
///
/// This is a regression test for https://github.com/dotnet/msbuild/issues/13013
/// </summary>
/// <param name="useMatchingRuntime">Whether to use a runtime that matches the current MSBuild runtime.</param>
/// <param name="explicitlyRequestTaskHostFactory">Whether to explicitly request TaskHostFactory.</param>
/// <param name="shouldBeOutOfProc">Whether the task should execute out of process (TaskHostTask).</param>
/// <param name="shouldUseSidecar">Whether the task host should use sidecar mode (long-lived with node reuse). Null if not out of proc.</param>
[Theory]
[InlineData(true, true, true, false)] // Scenario 1: Match + Explicit → short-lived out of proc
[InlineData(true, false, false, null)] // Scenario 2: Match + No Explicit → in-proc
[InlineData(false, true, true, false)] // Scenario 3: No Match + Explicit → short-lived out of proc
[InlineData(false, false, true, true)] // Scenario 4: No Match + No Explicit → long-lived sidecar out of proc
public void TaskHostLifecycle_ValidatesCorrectBehavior(
bool useMatchingRuntime,
bool explicitlyRequestTaskHostFactory,
bool shouldBeOutOfProc,
bool? shouldUseSidecar)
{
ITask createdTask = null;
try
{
// Setup: Configure runtime and TaskHostFactory request based on test parameters
string runtime = useMatchingRuntime
? XMakeAttributes.GetCurrentMSBuildRuntime()
: XMakeAttributes.MSBuildRuntimeValues.clr2;

TaskHostParameters factoryParameters = new(runtime);
SetupTaskFactory(factoryParameters, explicitlyRequestTaskHostFactory, explicitlyRequestTaskHostFactory);

createdTask = _taskFactory.CreateTaskInstance(
ElementLocation.Create("MSBUILD"),
null,
new MockHost(),
TaskHostParameters.Empty,
#if FEATURE_APPDOMAIN
new AppDomainSetup(),
#endif
false,
scheduledNodeId: 1,
(string propName) => ProjectPropertyInstance.Create("test", "test"),
CreateStubTaskEnvironment());

// Validate task creation
createdTask.ShouldNotBeNull();

// Validate whether task is out of proc (TaskHostTask) or in-proc
if (shouldBeOutOfProc)
{
createdTask.ShouldBeOfType<TaskHostTask>();

// Validate sidecar mode if out of proc
TaskHostTask taskHostTask = (TaskHostTask)createdTask;
bool useSidecarTaskHost = IsUsingSidecarMode(taskHostTask);

if (shouldUseSidecar == true)
{
useSidecarTaskHost.ShouldBeTrue("When TaskHostFactory is NOT explicitly requested and runtime doesn't match, should use long-lived sidecar task host (node reuse enabled)");
}
else
{
useSidecarTaskHost.ShouldBeFalse("When TaskHostFactory is explicitly requested, should use short-lived task host (no node reuse)");
}
}
else
{
createdTask.ShouldNotBeOfType<TaskHostTask>();
}
}
finally
{
if (createdTask != null)
{
_taskFactory.CleanupTask(createdTask);
}
}
}

/// <summary>
/// Helper method to check if a TaskHostTask is using sidecar mode (node reuse).
/// </summary>
/// <param name="taskHostTask">The TaskHostTask to check.</param>
/// <returns>True if using sidecar mode (long-lived, node reuse enabled), false otherwise.</returns>
private static bool IsUsingSidecarMode(TaskHostTask taskHostTask)
{
var useSidecarField = typeof(TaskHostTask).GetField("_useSidecarTaskHost", BindingFlags.NonPublic | BindingFlags.Instance);
useSidecarField.ShouldNotBeNull("_useSidecarTaskHost field should exist");
return (bool)useSidecarField.GetValue(taskHostTask);
}

/// <summary>
/// Abstract out the creation of the new AssemblyTaskFactory with default task, and
/// with some basic validation.
Expand Down
8 changes: 6 additions & 2 deletions src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,12 @@ internal ITask CreateTaskInstance(
mergedParameters = UpdateTaskHostParameters(mergedParameters);
(mergedParameters, bool isNetRuntime) = AddNetHostParamsIfNeeded(mergedParameters, getProperty);

bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false)
|| isNetRuntime;
// Sidecar here means that the task host is launched with /nodeReuse:true and doesn't terminate
// after the task execution. This improves performance for tasks that run multiple times in a build.
// If the task host factory is explicitly requested, do not act as a sidecar task host.
// This is important as customers use task host factories for short lived tasks to release
// potential locks.
bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false);

TaskHostTask task = new(
taskLocation,
Expand Down