Skip to content
Merged
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
270 changes: 46 additions & 224 deletions src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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 Microsoft.Build.Execution;
Expand Down Expand Up @@ -46,7 +45,6 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
{
using (TestEnvironment env = TestEnvironment.Create())
{
using ProcessTracker processTracker = new();
string taskFactory = taskHostFactorySpecified ? "TaskHostFactory" : "AssemblyTaskFactory";
string pidTaskProject = $@"
<Project>
Expand All @@ -64,9 +62,16 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
env.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1");
}

// To execute the task in sidecar mode, both node reuse and the environment variable must be set.
BuildParameters buildParameters = new() { EnableNodeReuse = envVariableSpecified && true /* node reuse enabled */ };

ProjectInstance projectInstance = new(project.Path);

projectInstance.Build().ShouldBeTrue();
BuildManager buildManager = BuildManager.DefaultBuildManager;
BuildResult buildResult = buildManager.Build(buildParameters, new BuildRequestData(projectInstance, targetsToBuild: ["AccessPID"]));

buildResult.OverallResult.ShouldBe(BuildResultCode.Success);

string processId = projectInstance.GetPropertyValue("PID");
string.IsNullOrEmpty(processId).ShouldBeFalse();
Int32.TryParse(processId, out int pid).ShouldBeTrue();
Expand All @@ -88,23 +93,18 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
}
else
{
// This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
Process taskHostNode = Process.GetProcessById(pid);
bool processExited = taskHostNode.WaitForExit(3000);

processExited.ShouldBeFalse();
try
{
// This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
Process taskHostNode = Process.GetProcessById(pid);
using var taskHostNodeTracker = processTracker.AttachToProcess(pid, "Sidecar", _output);
bool processExited = taskHostNode.WaitForExit(3000);
if (processExited)
{
processTracker.PrintSummary(_output);
}

processExited.ShouldBeFalse();
taskHostNode.Kill();
}
catch
{
processTracker.PrintSummary(_output);
// Ignore exceptions from Kill - the process may have exited between the WaitForExit and Kill calls.
}
}
}
Expand All @@ -119,9 +119,7 @@ public void TransientAndSidecarNodeCanCoexist()
{
using (TestEnvironment env = TestEnvironment.Create(_output))
{
using ProcessTracker processTracker = new();
{
string pidTaskProject = $@"
string pidTaskProject = $@"
<Project>
<UsingTask TaskName=""ProcessIdTask"" AssemblyName=""Microsoft.Build.Engine.UnitTests"" TaskFactory=""TaskHostFactory"" />
<UsingTask TaskName=""ProcessIdTaskSidecar"" AssemblyName=""Microsoft.Build.Engine.UnitTests"" TaskFactory=""AssemblyTaskFactory"" />
Expand All @@ -136,48 +134,44 @@ public void TransientAndSidecarNodeCanCoexist()
</Target>
</Project>";

TransientTestFile project = env.CreateFile("testProject.csproj", pidTaskProject);

env.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1");
ProjectInstance projectInstance = new(project.Path);
TransientTestFile project = env.CreateFile("testProject.csproj", pidTaskProject);

projectInstance.Build().ShouldBeTrue();
env.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1");
ProjectInstance projectInstance = new(project.Path);

string transientPid = projectInstance.GetPropertyValue("PID");
string sidecarPid = projectInstance.GetPropertyValue("PID2");
sidecarPid.ShouldNotBe(transientPid, "Each task should have it's own TaskHost node.");
projectInstance.Build().ShouldBeTrue();

using var sidecarTracker = processTracker.AttachToProcess(int.Parse(sidecarPid), "Sidecar", _output);
string transientPid = projectInstance.GetPropertyValue("PID");
string sidecarPid = projectInstance.GetPropertyValue("PID2");
sidecarPid.ShouldNotBe(transientPid, "Each task should have it's own TaskHost node.");

string.IsNullOrEmpty(transientPid).ShouldBeFalse();
Int32.TryParse(transientPid, out int pid).ShouldBeTrue();
Int32.TryParse(sidecarPid, out int pidSidecar).ShouldBeTrue();
string.IsNullOrEmpty(transientPid).ShouldBeFalse();
Int32.TryParse(transientPid, out int pid).ShouldBeTrue();
Int32.TryParse(sidecarPid, out int pidSidecar).ShouldBeTrue();

Process.GetCurrentProcess().Id.ShouldNotBe(pid);
Process.GetCurrentProcess().Id.ShouldNotBe(pid);

try
{
Process transientTaskHostNode = Process.GetProcessById(pid);
transientTaskHostNode.WaitForExit(3000).ShouldBeTrue("The node should be dead since this is the transient case.");
}
catch (ArgumentException e)
{
// We expect the TaskHostNode to exit quickly. If it exits before Process.GetProcessById, it will throw an ArgumentException.
e.Message.ShouldBe($"Process with an Id of {pid} is not running.");
}
try
{
Process transientTaskHostNode = Process.GetProcessById(pid);
transientTaskHostNode.WaitForExit(3000).ShouldBeTrue("The node should be dead since this is the transient case.");
}
catch (ArgumentException e)
{
// We expect the TaskHostNode to exit quickly. If it exits before Process.GetProcessById, it will throw an ArgumentException.
e.Message.ShouldBe($"Process with an Id of {pid} is not running.");
}

try
{
// This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
Process sidecarTaskHostNode = Process.GetProcessById(pidSidecar);
sidecarTaskHostNode.WaitForExit(3000).ShouldBeFalse($"The node should be alive since it is the sidecar node.");
sidecarTaskHostNode.Kill();
}
catch (Exception e)
{
processTracker.PrintSummary(_output);
e.Message.ShouldNotBe($"Process with an Id of {pidSidecar} is not running");
}
try
{
// This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
Process sidecarTaskHostNode = Process.GetProcessById(pidSidecar);
sidecarTaskHostNode.WaitForExit(3000).ShouldBeFalse($"The node should be alive since it is the sidecar node.");
sidecarTaskHostNode.Kill();
}
catch (Exception e)
{
e.Message.ShouldNotBe($"Process with an Id of {pidSidecar} is not running");
}
}
}
Expand Down Expand Up @@ -332,177 +326,5 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost()
projectInstance.GetPropertyValue("CustomStructOutput").ShouldBe(TaskBuilderTestTask.s_customStruct.ToString(CultureInfo.InvariantCulture));
projectInstance.GetPropertyValue("EnumOutput").ShouldBe(TargetBuiltReason.BeforeTargets.ToString());
}

/// <summary>
/// Helper class for tracking external processes during tests.
/// Monitors process lifecycle and provides diagnostic information for debugging.
/// </summary>
internal sealed class ProcessTracker : IDisposable
{
private readonly List<TrackedProcess> _trackedProcesses = new();

/// <summary>
/// Attaches to an existing process for monitoring.
/// </summary>
/// <param name="pid">Process ID to attach to</param>
/// <param name="name">Friendly name for the process</param>
/// <param name="output">Test output helper for logging</param>
/// <returns>TrackedProcess instance for the attached process</returns>
public TrackedProcess AttachToProcess(int pid, string name, ITestOutputHelper output)
{
try
{
var process = Process.GetProcessById(pid);
var tracked = new TrackedProcess(process, name);

// Enable event notifications
process.EnableRaisingEvents = true;

// Subscribe to exit event
process.Exited += (sender, e) =>
{
var proc = sender as Process;
tracked.ExitTime = DateTime.Now;
tracked.ExitCode = proc?.ExitCode ?? -999;
tracked.HasExited = true;

output.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {tracked.Name} (PID {tracked.ProcessId}) EXITED with code {tracked.ExitCode}");
};

_trackedProcesses.Add(tracked);
output.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] Attached to {name} (PID {pid})");

return tracked;
}
catch (ArgumentException)
{
output.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] Could not attach to {name} (PID {pid}) - process not found");
return new TrackedProcess(null, name) { ProcessId = pid, NotFound = true };
}
}

/// <summary>
/// Prints a summary of all tracked processes for diagnostic purposes.
/// </summary>
/// <param name="output">Test output helper for logging</param>
public void PrintSummary(ITestOutputHelper output)
{
output.WriteLine("\n=== PROCESS TRACKING SUMMARY ===");
foreach (var tracked in _trackedProcesses)
{
tracked.PrintStatus(output);
}
}

public void Dispose()
{
foreach (var tracked in _trackedProcesses)
{
tracked.Dispose();
}
}
}

/// <summary>
/// Represents a tracked process with lifecycle monitoring capabilities.
/// </summary>
internal sealed class TrackedProcess : IDisposable
{
/// <summary>
/// The underlying Process object being tracked.
/// </summary>
public Process Process { get; }

/// <summary>
/// Friendly name for the tracked process.
/// </summary>
public string Name { get; }

/// <summary>
/// Process ID of the tracked process.
/// </summary>
public int ProcessId { get; set; }

/// <summary>
/// Time when tracking began for this process.
/// </summary>
public DateTime AttachTime { get; }

/// <summary>
/// Time when the process exited, if applicable.
/// </summary>
public DateTime? ExitTime { get; set; }

/// <summary>
/// Exit code of the process, if it has exited.
/// </summary>
public int? ExitCode { get; set; }

/// <summary>
/// Whether the process has exited.
/// </summary>
public bool HasExited { get; set; }

/// <summary>
/// Whether the process was not found when attempting to attach.
/// </summary>
public bool NotFound { get; set; }

public TrackedProcess(Process process, string name)
{
Process = process;
Name = name;
ProcessId = process?.Id ?? -1;
AttachTime = DateTime.Now;
}

/// <summary>
/// Prints detailed status information about the tracked process.
/// </summary>
/// <param name="output">Test output helper for logging</param>
public void PrintStatus(ITestOutputHelper output)
{
output.WriteLine($"\n{Name} (PID {ProcessId}):");
output.WriteLine($" Attached at: {AttachTime:HH:mm:ss.fff}");

if (NotFound)
{
output.WriteLine(" Status: Not found when trying to attach");
}
else if (HasExited)
{
var duration = (ExitTime.Value - AttachTime).TotalMilliseconds;
output.WriteLine($" Status: Exited with code {ExitCode}");
output.WriteLine($" Exit time: {ExitTime:HH:mm:ss.fff}");
output.WriteLine($" Duration: {duration:F0}ms");
}
else
{
try
{
if (Process != null && !Process.HasExited)
{
output.WriteLine(" Status: Still running");
output.WriteLine($" Start time: {Process.StartTime:HH:mm:ss.fff}");
output.WriteLine($" CPU time: {Process.TotalProcessorTime.TotalMilliseconds:F0}ms");
}
else
{
output.WriteLine(" Status: Exited (detected during status check)");
if (Process != null)
{
output.WriteLine($" Exit code: {Process.ExitCode}");
}
}
}
catch (Exception ex)
{
output.WriteLine($" Status: Error checking process - {ex.Message}");
}
}
}

public void Dispose() => Process?.Dispose();
}
}
}