diff --git a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs index 6d9f78de3aa..6334bbb125a 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs @@ -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; @@ -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 = $@" @@ -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(); @@ -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. } } } @@ -119,9 +119,7 @@ public void TransientAndSidecarNodeCanCoexist() { using (TestEnvironment env = TestEnvironment.Create(_output)) { - using ProcessTracker processTracker = new(); - { - string pidTaskProject = $@" + string pidTaskProject = $@" @@ -136,48 +134,44 @@ public void TransientAndSidecarNodeCanCoexist() "; - 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"); } } } @@ -332,177 +326,5 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost() projectInstance.GetPropertyValue("CustomStructOutput").ShouldBe(TaskBuilderTestTask.s_customStruct.ToString(CultureInfo.InvariantCulture)); projectInstance.GetPropertyValue("EnumOutput").ShouldBe(TargetBuiltReason.BeforeTargets.ToString()); } - - /// - /// Helper class for tracking external processes during tests. - /// Monitors process lifecycle and provides diagnostic information for debugging. - /// - internal sealed class ProcessTracker : IDisposable - { - private readonly List _trackedProcesses = new(); - - /// - /// Attaches to an existing process for monitoring. - /// - /// Process ID to attach to - /// Friendly name for the process - /// Test output helper for logging - /// TrackedProcess instance for the attached process - 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 }; - } - } - - /// - /// Prints a summary of all tracked processes for diagnostic purposes. - /// - /// Test output helper for logging - 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(); - } - } - } - - /// - /// Represents a tracked process with lifecycle monitoring capabilities. - /// - internal sealed class TrackedProcess : IDisposable - { - /// - /// The underlying Process object being tracked. - /// - public Process Process { get; } - - /// - /// Friendly name for the tracked process. - /// - public string Name { get; } - - /// - /// Process ID of the tracked process. - /// - public int ProcessId { get; set; } - - /// - /// Time when tracking began for this process. - /// - public DateTime AttachTime { get; } - - /// - /// Time when the process exited, if applicable. - /// - public DateTime? ExitTime { get; set; } - - /// - /// Exit code of the process, if it has exited. - /// - public int? ExitCode { get; set; } - - /// - /// Whether the process has exited. - /// - public bool HasExited { get; set; } - - /// - /// Whether the process was not found when attempting to attach. - /// - public bool NotFound { get; set; } - - public TrackedProcess(Process process, string name) - { - Process = process; - Name = name; - ProcessId = process?.Id ?? -1; - AttachTime = DateTime.Now; - } - - /// - /// Prints detailed status information about the tracked process. - /// - /// Test output helper for logging - 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(); - } } }