diff --git a/documentation/wiki/Building-Testing-and-Debugging-on-.Net-Core-MSBuild.md b/documentation/wiki/Building-Testing-and-Debugging-on-.Net-Core-MSBuild.md
index fd3c5ca1717..ef1f121b307 100644
--- a/documentation/wiki/Building-Testing-and-Debugging-on-.Net-Core-MSBuild.md
+++ b/documentation/wiki/Building-Testing-and-Debugging-on-.Net-Core-MSBuild.md
@@ -40,7 +40,12 @@ The best way to get .NET Core MSBuild is by installing the [.NET Core SDK](https
## Wait in Main
-Set the environment variable `MSBUILDDEBUGONSTART` to `2`, then attach a debugger to the process manually after it starts.
+Set the environment variable `MSBUILDDEBUGONSTART` to control debugger behavior at startup:
+- `1`: Launch debugger for all processes (main MSBuild and TaskHost child processes)
+- `2`: Wait for manual debugger attach for all processes
+- `3`: Launch debugger for main MSBuild process only, skip TaskHost child processes
+
+For example, set `MSBUILDDEBUGONSTART` to `2`, then attach a debugger to the process manually after it starts.
## Using the repository binaries to perform builds
diff --git a/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md b/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md
index 43134c57c3b..b4fe8cc796b 100644
--- a/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md
+++ b/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md
@@ -81,7 +81,10 @@ Sometimes it's useful to patch your copy of Visual Studio in order to test or de
### Debugging MSBuild
#### Breakpoints
-To break into the [main method](https://github.com/dotnet/msbuild/blob/bd00d6cba24d41efd6f54699c3fdbefb9f5034a1/src/MSBuild/XMake.cs#L493-L506) of MSBuild.exe: set the environment variable `MSBUILDDEBUGONSTART` to 1 (uses `Debugger.Launch()`) or 2 (waits until debugger is attached).
+To break into the [main method](https://github.com/dotnet/msbuild/blob/bd00d6cba24d41efd6f54699c3fdbefb9f5034a1/src/MSBuild/XMake.cs#L493-L506) of MSBuild.exe: set the environment variable `MSBUILDDEBUGONSTART` to:
+- `1`: Launch debugger for all processes (main MSBuild and TaskHost child processes) using `Debugger.Launch()`
+- `2`: Wait for manual debugger attach for all processes
+- `3`: Launch debugger for main MSBuild process only using `Debugger.Launch()`, skip TaskHost child processes
To break into MSBuild's [BuildManager.BeginBuild](https://github.com/dotnet/msbuild/blob/bd00d6cba24d41efd6f54699c3fdbefb9f5034a1/src/Build/BackEnd/BuildManager/BuildManager.cs#L414) set the environment variable `MSBuildDebugBuildManagerOnStart` to 1 (uses `Debugger.Launch()`) or 2 (waits until debugger is attached).
This is useful for debugging MSBuild when it is called from other apps that use its APIs instead of its executable (for example Visual Studio). You can also filter which processes trigger the breakpoint by setting `MSBuildDebugProcessName` to a substring of the process name. For example, to trigger the breakpoint only under Visual Studio's top level process you would set `MSBuildDebugProcessName` to the value `devenv`.
diff --git a/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs b/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs
index bb9114e0791..b2f292a8511 100644
--- a/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs
+++ b/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using Microsoft.Build.Shared;
+using Microsoft.Build.Shared.Debugging;
using Shouldly;
using Xunit;
@@ -38,5 +39,13 @@ public void DumpExceptionToFileShouldWriteInDebugDumpPath()
File.Delete(exceptionFile);
}
}
+
+ [Fact]
+ public void IsInTaskHostNode_ReturnsFalseForCentralNode()
+ {
+ // When running in the main test process (no /nodemode argument),
+ // we should not be in a TaskHost node
+ DebugUtils.IsInTaskHostNode().ShouldBeFalse();
+ }
}
}
diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs
index 7febc492d7a..a8989189cf4 100644
--- a/src/MSBuild/XMake.cs
+++ b/src/MSBuild/XMake.cs
@@ -597,6 +597,13 @@ private static void DebuggerLaunchCheck()
case "1":
Debugger.Launch();
break;
+ case "3":
+ // Value "3" debugs the main MSBuild process but skips debugging child TaskHost processes
+ if (!DebugUtils.IsInTaskHostNode())
+ {
+ Debugger.Launch();
+ }
+ break;
#endif
case "2":
// Sometimes easier to attach rather than deal with JIT prompt
diff --git a/src/MSBuildTaskHost/OutOfProcTaskHost.cs b/src/MSBuildTaskHost/OutOfProcTaskHost.cs
index 90a7970b5e7..9ee7b001bca 100644
--- a/src/MSBuildTaskHost/OutOfProcTaskHost.cs
+++ b/src/MSBuildTaskHost/OutOfProcTaskHost.cs
@@ -94,6 +94,10 @@ internal static ExitType Execute()
Console.ReadLine();
break;
+ case "3":
+ // Value "3" skips debugging for TaskHost processes but debugs the main MSBuild process
+ // This is useful when you want to debug MSBuild but not the child TaskHost processes
+ break;
}
bool restart = false;
diff --git a/src/Shared/Debugging/DebugUtils.cs b/src/Shared/Debugging/DebugUtils.cs
index ff9219a6d5b..40750e307aa 100644
--- a/src/Shared/Debugging/DebugUtils.cs
+++ b/src/Shared/Debugging/DebugUtils.cs
@@ -105,6 +105,16 @@ private static bool CurrentProcessMatchesDebugName()
public static string DebugPath { get; private set; }
+ ///
+ /// Returns true if the current process is an out-of-proc TaskHost node.
+ ///
+ ///
+ /// True if this process was launched with /nodemode:2 (indicating it's a TaskHost process),
+ /// false otherwise. This is useful for conditionally enabling debugging or other behaviors
+ /// based on whether the code is running in the main MSBuild process or a child TaskHost process.
+ ///
+ public static bool IsInTaskHostNode() => ProcessNodeMode.Value == NodeMode.OutOfProcTaskHostNode;
+
public static string FindNextAvailableDebugFilePath(string fileName)
{
var extension = Path.GetExtension(fileName);