diff --git a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj
index 21f20586dbd..fc675e1852f 100644
--- a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj
+++ b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj
@@ -51,7 +51,6 @@
-
App.config
Designer
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
index 66f78b30ec2..b74013ba0f0 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
@@ -32,7 +32,7 @@ namespace Microsoft.Build.BackEnd
/// Contains the shared pieces of code from NodeProviderOutOfProc
/// and NodeProviderOutOfProcTaskHost.
///
- internal abstract class NodeProviderOutOfProcBase
+ internal abstract partial class NodeProviderOutOfProcBase
{
///
/// The maximum number of bytes to write
@@ -218,6 +218,10 @@ protected IList GetNodes(
}
bool nodeReuseRequested = Handshake.IsHandshakeOptionEnabled(hostHandshake.HandshakeOptions, HandshakeOptions.NodeReuse);
+
+ // Extract the expected NodeMode from the command line arguments
+ NodeMode? expectedNodeMode = NodeModeHelper.ExtractFromCommandLine(commandLineArgs);
+
// Get all process of possible running node processes for reuse and put them into ConcurrentQueue.
// Processes from this queue will be concurrently consumed by TryReusePossibleRunningNodes while
// trying to connect to them and reuse them. When queue is empty, no process to reuse left
@@ -229,7 +233,7 @@ protected IList GetNodes(
if (nodeReuseRequested)
{
IList possibleRunningNodesList;
- (expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation);
+ (expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation, expectedNodeMode);
possibleRunningNodes = new ConcurrentQueue(possibleRunningNodesList);
if (possibleRunningNodesList.Count > 0)
@@ -395,14 +399,17 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
}
///
- /// Finds processes named after either msbuild or msbuildtaskhost.
+ /// Finds processes that could be reusable MSBuild nodes.
+ /// Discovers both msbuild.exe processes and dotnet processes hosting MSBuild.dll.
+ /// Filters candidates by NodeMode when available.
///
- ///
+ /// The location of the MSBuild executable
+ /// The NodeMode to filter for, or null to include all
///
- /// Item 1 is the name of the process being searched for.
- /// Item 2 is the ConcurrentQueue of ordered processes themselves.
+ /// Item 1 is a descriptive name of the processes being searched for.
+ /// Item 2 is the list of matching processes, sorted by ID.
///
- private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null)
+ private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null, NodeMode? expectedNodeMode = null)
{
if (String.IsNullOrEmpty(msbuildLocation))
{
@@ -410,10 +417,84 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
}
var expectedProcessName = Path.GetFileNameWithoutExtension(CurrentHost.GetCurrentHost() ?? msbuildLocation);
+
+ // Get all processes with the expected MSBuild executable name
+ Process[] processes;
+ try
+ {
+ processes = Process.GetProcessesByName(expectedProcessName);
+ }
+ catch
+ {
+ // Process enumeration failed, return empty list
+ return (expectedProcessName, Array.Empty());
+ }
- var processes = Process.GetProcessesByName(expectedProcessName);
- Array.Sort(processes, (left, right) => left.Id.CompareTo(right.Id));
+ // If we have an expected NodeMode, filter by command line parsing
+ if (expectedNodeMode.HasValue && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5))
+ {
+ CommunicationsUtilities.Trace("Filtering {0} candidate processes by NodeMode {1} for process name '{2}'",
+ processes.Length, expectedNodeMode.Value, expectedProcessName);
+ List filteredProcesses = [];
+ bool isDotnetProcess = expectedProcessName.Equals(Path.GetFileNameWithoutExtension(Constants.DotnetProcessName), StringComparison.OrdinalIgnoreCase);
+
+ foreach (var process in processes)
+ {
+ try
+ {
+ if (!process.TryGetCommandLine(out string commandLine))
+ {
+ // If we can't get the command line, skip this process
+ CommunicationsUtilities.Trace("Skipping process {0} - unable to retrieve command line", process.Id);
+ continue;
+ }
+
+ if (commandLine is null)
+ {
+ // If we can't get the command line, then allow it as a candidate. This allows reuse to work on platforms where command line retrieval isn't supported, but still filters by NodeMode on platforms where it is supported.
+ CommunicationsUtilities.Trace("Including process {0} with unknown NodeMode because command line retrieval is not supported on this platform", process.Id);
+ filteredProcesses.Add(process);
+ continue;
+ }
+
+ // If expected process is dotnet, filter to only those hosting MSBuild.dll
+ if (isDotnetProcess && !commandLine.Contains("MSBuild.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ CommunicationsUtilities.Trace("Skipping dotnet process {0} - not hosting MSBuild.dll. Command line: {1}", process.Id, commandLine);
+ continue;
+ }
+
+ // Extract NodeMode from command line
+ NodeMode? processNodeMode = NodeModeHelper.ExtractFromCommandLine(commandLine);
+
+ // Only include processes that match the expected NodeMode
+ if (processNodeMode.HasValue && processNodeMode.Value == expectedNodeMode.Value)
+ {
+ CommunicationsUtilities.Trace("Including process {0} with matching NodeMode {1}", process.Id, processNodeMode.Value);
+ filteredProcesses.Add(process);
+ }
+ else
+ {
+ CommunicationsUtilities.Trace("Skipping process {0} - NodeMode mismatch. Expected: {1}, Found: {2}. Command line: {3}",
+ process.Id, expectedNodeMode.Value, processNodeMode?.ToString() ?? "", commandLine);
+ }
+ }
+ catch (Exception ex)
+ {
+ // If we encounter any error processing this process, skip it but log
+ CommunicationsUtilities.Trace("Failed to get command line for process {0}: {1}", process.Id, ex.Message);
+ continue;
+ }
+ }
+
+ // Sort by process ID for consistent ordering
+ filteredProcesses.Sort((left, right) => left.Id.CompareTo(right.Id));
+ CommunicationsUtilities.Trace("Filtered to {0} processes matching NodeMode {1}", filteredProcesses.Count, expectedNodeMode.Value);
+ return (expectedProcessName, filteredProcesses);
+ }
+ // No NodeMode filtering, return all processes sorted by ID
+ Array.Sort(processes, (left, right) => left.Id.CompareTo(right.Id));
return (expectedProcessName, processes);
}
diff --git a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj
index aba4ae6c2d2..d31ef573b24 100644
--- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj
+++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj
@@ -29,7 +29,6 @@
-
diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs
index e5858135cf0..4ae854ba1b4 100644
--- a/src/Framework/NativeMethods.cs
+++ b/src/Framework/NativeMethods.cs
@@ -772,6 +772,7 @@ internal static bool IsLinux
///
/// Gets a flag indicating if we are running under flavor of BSD (NetBSD, OpenBSD, FreeBSD)
///
+ [SupportedOSPlatformGuard("freebsd")]
internal static bool IsBSD
{
#if CLR2COMPATIBILITY
@@ -813,6 +814,7 @@ internal static bool IsWindows
///
/// Gets a flag indicating if we are running under Mac OSX
///
+ [SupportedOSPlatformGuard("macos")]
internal static bool IsOSX
{
#if CLR2COMPATIBILITY
diff --git a/src/Framework/NodeMode.cs b/src/Framework/NodeMode.cs
index 97c60854006..87d5d5bdb05 100644
--- a/src/Framework/NodeMode.cs
+++ b/src/Framework/NodeMode.cs
@@ -36,7 +36,7 @@ internal enum NodeMode
///
/// Helper methods for the NodeMode enum.
///
- internal static class NodeModeHelper
+ internal static partial class NodeModeHelper
{
///
/// Converts a NodeMode value to a command line argument string.
@@ -53,12 +53,45 @@ internal static class NodeModeHelper
/// True if parsing succeeded, false otherwise
public static bool TryParse(string value, [NotNullWhen(true)] out NodeMode? nodeMode)
{
+#if NET
+ return TryParseImpl(value.AsSpan(), out nodeMode);
+#else
+ return TryParseImpl(value, out nodeMode);
+#endif
+ }
+
+#if NET
+ ///
+ /// Tries to parse a node mode value from a span, supporting both integer values and enum names (case-insensitive).
+ ///
+ /// The value to parse (can be an integer or enum name)
+ /// The parsed NodeMode value if successful
+ /// True if parsing succeeded, false otherwise
+ public static bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out NodeMode? nodeMode)
+ {
+ return TryParseImpl(value, out nodeMode);
+ }
+#endif
+
+#if NET
+ private static bool TryParseImpl(ReadOnlySpan value, [NotNullWhen(true)] out NodeMode? nodeMode)
+#else
+ private static bool TryParseImpl(string value, [NotNullWhen(true)] out NodeMode? nodeMode)
+#endif
+ {
nodeMode = null;
+#if NET
+ if (value.IsEmpty || value.IsWhiteSpace())
+ {
+ return false;
+ }
+#else
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
+#endif
// First try to parse as an integer for backward compatibility
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
@@ -69,7 +102,7 @@ public static bool TryParse(string value, [NotNullWhen(true)] out NodeMode? node
nodeMode = (NodeMode)intValue;
return true;
}
-
+
return false;
}
@@ -82,5 +115,47 @@ public static bool TryParse(string value, [NotNullWhen(true)] out NodeMode? node
return false;
}
+
+ ///
+ /// Extracts the NodeMode from a command line string using regex pattern matching.
+ ///
+ /// The command line to parse. Note that this can't be a span because generated regex don't have a Span Match overload
+ /// The NodeMode if found, otherwise null
+ public static NodeMode? ExtractFromCommandLine(string commandLine)
+ {
+ if (string.IsNullOrWhiteSpace(commandLine))
+ {
+ return null;
+ }
+
+ var match = CommandLineNodeModeRegex.Match(commandLine);
+
+ if (!match.Success)
+ {
+ return null;
+ }
+
+#if NET
+ if (TryParse(match.Groups["nodemode"].ValueSpan, out NodeMode? nodeMode))
+#else
+ if (TryParse(match.Groups["nodemode"].Value, out NodeMode? nodeMode))
+#endif
+ {
+ return nodeMode;
+ }
+
+ return null;
+ }
+
+ private const string CommandLineNodeModePattern = @"/nodemode:(?[a-zA-Z0-9]+)(?:\s|$)";
+
+#if NET
+ [System.Text.RegularExpressions.GeneratedRegex(CommandLineNodeModePattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase)]
+ private static partial System.Text.RegularExpressions.Regex CommandLineNodeModeRegex { get; }
+#else
+ private static System.Text.RegularExpressions.Regex CommandLineNodeModeRegex { get; } = new(
+ CommandLineNodeModePattern,
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled);
+#endif
}
}
diff --git a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj
index 4ba8bcc482e..378a9068711 100644
--- a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj
+++ b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj
@@ -4,6 +4,7 @@
$(RuntimeOutputTargetFrameworks)
$(RuntimeOutputPlatformTarget)
false
+ true
diff --git a/src/Shared/Debugging/DebugUtils.cs b/src/Shared/Debugging/DebugUtils.cs
index 87ed7798304..5dfb1573ae4 100644
--- a/src/Shared/Debugging/DebugUtils.cs
+++ b/src/Shared/Debugging/DebugUtils.cs
@@ -2,9 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Diagnostics;
using System.IO;
-using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared.FileSystem;
@@ -57,32 +55,7 @@ internal static void SetDebugPath()
}
private static readonly Lazy ProcessNodeMode = new(
- () =>
- {
- return ScanNodeMode(Environment.CommandLine);
-
- NodeMode? ScanNodeMode(string input)
- {
- var match = Regex.Match(input, @"/nodemode:(?[1-9]\d*)(\s|$)", RegexOptions.IgnoreCase);
-
- if (!match.Success)
- {
- return null; // Central/main process (not running as a node)
- }
- var nodeMode = match.Groups["nodemode"].Value;
-
- Trace.Assert(!string.IsNullOrEmpty(nodeMode));
-
- // Try to parse using the shared NodeModeHelper
- if (NodeModeHelper.TryParse(nodeMode, out NodeMode? parsedMode))
- {
- return parsedMode;
- }
-
- // If parsing fails, this is an unknown/unsupported node mode
- return null;
- }
- });
+ () => NodeModeHelper.ExtractFromCommandLine(Environment.CommandLine));
private static bool CurrentProcessMatchesDebugName()
{
diff --git a/src/Shared/ProcessExtensions.cs b/src/Shared/ProcessExtensions.cs
index 91addd48495..7ddd8dc6ab6 100644
--- a/src/Shared/ProcessExtensions.cs
+++ b/src/Shared/ProcessExtensions.cs
@@ -1,12 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+#if NET
+using System.Buffers;
+using System.Text;
+using System.IO;
+#endif
namespace Microsoft.Build.Shared
{
- internal static class ProcessExtensions
+ internal static partial class ProcessExtensions
{
public static void KillTree(this Process process, int timeoutMilliseconds)
{
@@ -17,10 +25,9 @@ public static void KillTree(this Process process, int timeoutMilliseconds)
{
try
{
- // issue the kill command
NativeMethodsShared.KillTree(process.Id);
}
- catch (System.InvalidOperationException)
+ catch (InvalidOperationException)
{
// The process already exited, which is fine,
// just continue.
@@ -28,13 +35,806 @@ public static void KillTree(this Process process, int timeoutMilliseconds)
}
else
{
- throw new System.NotSupportedException();
+ throw new NotSupportedException();
}
#endif
- // wait until the process finishes exiting/getting killed.
- // We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enough
+ // Wait until the process finishes exiting/getting killed.
+ // We don't want to wait forever here because the task is already supposed to be dying, we just want to give it long enough
// to try and flush what it can and stop. If it cannot do that in a reasonable time frame then we will just ignore it.
process.WaitForExit(timeoutMilliseconds);
}
+
+ ///
+ /// Retrieves the full command line for a process in a cross-platform manner.
+ ///
+ /// The process to get the command line for.
+ /// The command line string, or null if it cannot be retrieved.
+ /// True if the command line was successfully retrieved or the current platform doesn't support retrieving command lines, false if there was an error retrieving the command line.
+ public static bool TryGetCommandLine(this Process? process, out string? commandLine)
+ {
+ commandLine = null;
+
+ if (process?.HasExited != false)
+ {
+ return false;
+ }
+
+ try
+ {
+#if NET
+ if (NativeMethodsShared.IsWindows)
+ {
+ commandLine = Windows.GetCommandLine(process.Id);
+ return true;
+ }
+ else if (NativeMethodsShared.IsOSX || NativeMethodsShared.IsBSD)
+ {
+ commandLine = BSD.GetCommandLine(process.Id);
+ return true;
+ }
+ else if (NativeMethodsShared.IsLinux)
+ {
+ commandLine = Linux.GetCommandLine(process.Id);
+ return true;
+ }
+ else
+ {
+ // Unsupported OS - return false to fall back to prior behavior
+ commandLine = null;
+ return true;
+ }
+#else
+ commandLine = Windows.GetCommandLine(process.Id);
+ return true;
+#endif
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+#if NET
+ ///
+ /// Parses a null-separated byte buffer into a space-joined argument string using span-based slicing.
+ /// Used by both Linux (/proc/pid/cmdline) and macOS/BSD (sysctl KERN_PROCARGS2) parsing.
+ /// Uses ArrayPool to rent char buffers for efficient UTF-8 decoding without intermediate string allocations.
+ ///
+ private static string ParseNullSeparatedArguments(ReadOnlySpan data, int maxArgs = int.MaxValue)
+ {
+ if (data.IsEmpty)
+ {
+ return string.Empty;
+ }
+
+ // Rent a char buffer for UTF-8 decoding (max char count equals byte count for ASCII-like content)
+ char[] charBuffer = ArrayPool.Shared.Rent(data.Length);
+ try
+ {
+ int totalChars = 0;
+ int argsFound = 0;
+
+ while (!data.IsEmpty && argsFound < maxArgs)
+ {
+ int nullIndex = data.IndexOf((byte)0);
+ ReadOnlySpan segment = nullIndex >= 0 ? data.Slice(0, nullIndex) : data;
+
+ if (!segment.IsEmpty)
+ {
+ // Add space separator between arguments
+ if (totalChars > 0)
+ {
+ charBuffer[totalChars++] = ' ';
+ }
+
+ // Decode UTF-8 directly into the char buffer
+ int charsWritten = Encoding.UTF8.GetChars(segment, charBuffer.AsSpan(totalChars));
+
+ // UTF-8 decoder converts null bytes to null chars - replace them with spaces for safety
+ Span decodedChars = charBuffer.AsSpan(totalChars, charsWritten);
+ for (int i = 0; i < decodedChars.Length; i++)
+ {
+ if (decodedChars[i] == '\0')
+ {
+ decodedChars[i] = ' ';
+ }
+ }
+
+ totalChars += charsWritten;
+ argsFound++;
+ }
+
+ if (nullIndex < 0)
+ {
+ break;
+ }
+
+ data = data.Slice(nullIndex + 1);
+ }
+
+ return new string(charBuffer, 0, totalChars);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(charBuffer);
+ }
+ }
+#endif
+
+ ///
+ /// Windows-specific command line retrieval via WMI COM interfaces.
+ /// Queries Win32_Process for the CommandLine property using IWbemLocator/IWbemServices.
+ ///
+ [SupportedOSPlatform("windows")]
+ private static class Windows
+ {
+ // WMI COM interface GUIDs
+ private static readonly Guid CLSID_WbemLocator = new Guid("4590F811-1D3A-11D0-891F-00AA004B2E24");
+ private static readonly Guid IID_IWbemLocator = new Guid("DC12A687-737F-11CF-884D-00AA004B2E24");
+
+ // WBEM status codes
+ private const int WBEM_S_NO_ERROR = 0;
+ private const int WBEM_S_FALSE = 1; // No more objects in enumeration
+ private const int WBEM_FLAG_FORWARD_ONLY = 0x00000020;
+ private const int WBEM_FLAG_RETURN_IMMEDIATELY = 0x00000010;
+ private const int WBEM_INFINITE = -1;
+
+
+ // RPC authentication/impersonation constants (used by CoInitializeSecurity and CoSetProxyBlanket)
+ private const int RPC_C_AUTHN_LEVEL_DEFAULT = 0;
+ private const int RPC_C_AUTHN_LEVEL_CALL = 3;
+ private const int RPC_C_IMP_LEVEL_IMPERSONATE = 3;
+ private const int RPC_C_AUTHN_WINNT = 10;
+ private const int RPC_C_AUTHZ_NONE = 0;
+ private const int EOAC_NONE = 0;
+
+ // CoCreateInstance: in-process server
+ private const int CLSCTX_INPROC_SERVER = 1;
+
+ // HRESULTs for conditions that are not fatal failures
+ private const int RPC_E_TOO_LATE = unchecked((int)0x80010119); // CoInitializeSecurity already called
+
+ [DllImport("ole32.dll")]
+ private static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);
+
+ [DllImport("ole32.dll")]
+ private static extern int CoInitializeSecurity(
+ IntPtr pSecDesc,
+ int cAuthSvc,
+ IntPtr asAuthSvc,
+ IntPtr pReserved,
+ int dwAuthnLevel,
+ int dwImpLevel,
+ IntPtr pAuthList,
+ int dwCapabilities,
+ IntPtr pReserved3);
+
+ [DllImport("ole32.dll")]
+ private static extern int CoCreateInstance(
+ ref Guid rclsid,
+ IntPtr pUnkOuter,
+ int dwClsContext,
+ ref Guid riid,
+ [MarshalAs(UnmanagedType.Interface)] out IWbemLocator ppv);
+
+ [DllImport("ole32.dll")]
+ private static extern int CoSetProxyBlanket(
+ [MarshalAs(UnmanagedType.IUnknown)] object pProxy,
+ int dwAuthnSvc,
+ int dwAuthzSvc,
+ IntPtr pServerPrincName,
+ int dwAuthnLevel,
+ int dwImpLevel,
+ IntPtr pAuthInfo,
+ int dwCapabilities);
+
+#if !NET
+ [DllImport("ole32.dll", EntryPoint = "CoSetProxyBlanket")]
+ private static extern int CoSetProxyBlanketPtr(
+ IntPtr pProxy,
+ int dwAuthnSvc,
+ int dwAuthzSvc,
+ IntPtr pServerPrincName,
+ int dwAuthnLevel,
+ int dwImpLevel,
+ IntPtr pAuthInfo,
+ int dwCapabilities);
+#endif
+
+ [ComImport]
+ [Guid("DC12A687-737F-11CF-884D-00AA004B2E24")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IWbemLocator
+ {
+ [PreserveSig]
+ int ConnectServer(
+ [MarshalAs(UnmanagedType.BStr)] string strNetworkResource,
+ [MarshalAs(UnmanagedType.BStr)] string? strUser,
+ [MarshalAs(UnmanagedType.BStr)] string? strPassword,
+ [MarshalAs(UnmanagedType.BStr)] string? strLocale,
+ int lSecurityFlags,
+ [MarshalAs(UnmanagedType.BStr)] string? strAuthority,
+ IntPtr pCtx,
+ [MarshalAs(UnmanagedType.Interface)] out IWbemServices ppNamespace);
+ }
+
+ [Guid("44ACA674-E8FC-11D0-A07C-00C04FB68820")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [ComImport]
+ internal interface IWbemContext
+ {
+ [PreserveSig]
+ int Clone([MarshalAs(UnmanagedType.Interface)] out IWbemContext ppNewCopy);
+
+ [PreserveSig]
+ int GetNames(int lFlags, IntPtr pNames);
+
+ [PreserveSig]
+ int BeginEnumeration(int lFlags);
+
+ [PreserveSig]
+ int Next(int lFlags, [MarshalAs(UnmanagedType.BStr)] out string pstrName, IntPtr pValue);
+
+ [PreserveSig]
+ int EndEnumeration();
+
+ [PreserveSig]
+ int SetValue([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags, IntPtr pValue);
+
+ [PreserveSig]
+ int GetValue([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags, IntPtr pValue);
+
+ [PreserveSig]
+ int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags);
+
+ [PreserveSig]
+ int DeleteAll();
+ }
+
+ [ComImport]
+ [Guid("9556DC99-828C-11CF-A37E-00AA003240C7")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IWbemServices
+ {
+ [PreserveSig]
+ int OpenNamespace(
+ [MarshalAs(UnmanagedType.BStr)] string strNamespace,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr ppWorkingNamespace,
+ IntPtr ppResult);
+
+ [PreserveSig]
+ int CancelAsyncCall(IntPtr pSink);
+
+ [PreserveSig]
+ int QueryObjectSink(int lFlags, IntPtr ppResponseHandler);
+
+ [PreserveSig]
+ int GetObject(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr ppObject,
+ IntPtr ppCallResult);
+
+ [PreserveSig]
+ int GetObjectAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int PutClass(IntPtr pObject, int lFlags, IntPtr pCtx, IntPtr ppCallResult);
+
+ [PreserveSig]
+ int PutClassAsync(IntPtr pObject, int lFlags, IntPtr pCtx, IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int DeleteClass(
+ [MarshalAs(UnmanagedType.BStr)] string strClass,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr ppCallResult);
+
+ [PreserveSig]
+ int DeleteClassAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strClass,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int CreateClassEnum(
+ [MarshalAs(UnmanagedType.BStr)] string strSuperclass,
+ int lFlags,
+ IntPtr pCtx,
+ [MarshalAs(UnmanagedType.Interface)] out IEnumWbemClassObject ppEnum);
+
+ [PreserveSig]
+ int CreateClassEnumAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strSuperclass,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int PutInstance(IntPtr pInst, int lFlags, IntPtr pCtx, IntPtr ppCallResult);
+
+ [PreserveSig]
+ int PutInstanceAsync(IntPtr pInst, int lFlags, IntPtr pCtx, IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int DeleteInstance(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr ppCallResult);
+
+ [PreserveSig]
+ int DeleteInstanceAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int CreateInstanceEnum(
+ [MarshalAs(UnmanagedType.BStr)] string strFilter,
+ int lFlags,
+ IntPtr pCtx,
+ [MarshalAs(UnmanagedType.Interface)] out IEnumWbemClassObject ppEnum);
+
+ [PreserveSig]
+ int CreateInstanceEnumAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strFilter,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int ExecQuery(
+ [In][MarshalAs(UnmanagedType.BStr)] string strQueryLanguage,
+ [In][MarshalAs(UnmanagedType.BStr)] string strQuery,
+ [In] int lFlags,
+ [In] IWbemContext? pCtx,
+ [MarshalAs(UnmanagedType.Interface)] out IEnumWbemClassObject ppEnum);
+
+ [PreserveSig]
+ int ExecQueryAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strQueryLanguage,
+ [MarshalAs(UnmanagedType.BStr)] string strQuery,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int ExecNotificationQuery(
+ [MarshalAs(UnmanagedType.BStr)] string strQueryLanguage,
+ [MarshalAs(UnmanagedType.BStr)] string strQuery,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr ppEnum);
+
+ [PreserveSig]
+ int ExecNotificationQueryAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strQueryLanguage,
+ [MarshalAs(UnmanagedType.BStr)] string strQuery,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pResponseHandler);
+
+ [PreserveSig]
+ int ExecMethod(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ [MarshalAs(UnmanagedType.BStr)] string strMethodName,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pInParams,
+ IntPtr ppOutParams,
+ IntPtr ppCallResult);
+
+ [PreserveSig]
+ int ExecMethodAsync(
+ [MarshalAs(UnmanagedType.BStr)] string strObjectPath,
+ [MarshalAs(UnmanagedType.BStr)] string strMethodName,
+ int lFlags,
+ IntPtr pCtx,
+ IntPtr pInParams,
+ IntPtr pResponseHandler);
+ }
+
+ [ComImport]
+ [Guid("027947E1-D731-11CE-A357-000000000001")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IEnumWbemClassObject
+ {
+ [PreserveSig]
+ int Reset();
+
+ [PreserveSig]
+ int Next(
+ int lTimeout,
+ uint uCount,
+ [MarshalAs(UnmanagedType.Interface)] out IWbemClassObject apObjects,
+ out uint puReturned);
+
+ [PreserveSig]
+ int NextAsync(uint uCount, IntPtr pSink);
+
+ [PreserveSig]
+ int Clone([MarshalAs(UnmanagedType.Interface)] out IEnumWbemClassObject ppEnum);
+
+ [PreserveSig]
+ int Skip(int lTimeout, uint nCount);
+ }
+
+ [ComImport]
+ [Guid("DC12A681-737F-11CF-884D-00AA004B2E24")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IWbemClassObject
+ {
+ [PreserveSig]
+ int GetQualifierSet(IntPtr ppQualSet);
+
+ [PreserveSig]
+ int Get(
+ [MarshalAs(UnmanagedType.LPWStr)] string wszName,
+ int lFlags,
+ ref object pVal,
+ IntPtr pType,
+ IntPtr plFlavor);
+
+ [PreserveSig]
+ int Put([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags, ref object pVal, int type);
+
+ [PreserveSig]
+ int Delete([MarshalAs(UnmanagedType.LPWStr)] string wszName);
+
+ [PreserveSig]
+ int GetNames([MarshalAs(UnmanagedType.LPWStr)] string wszQualifierName, int lFlags, ref object pQualifierVal, IntPtr pNames);
+
+ [PreserveSig]
+ int BeginEnumeration(int lEnumFlags);
+
+ [PreserveSig]
+ int Next(int lFlags, [MarshalAs(UnmanagedType.BStr)] out string strName, ref object pVal, IntPtr pType, IntPtr plFlavor);
+
+ [PreserveSig]
+ int EndEnumeration();
+
+ [PreserveSig]
+ int GetPropertyQualifierSet([MarshalAs(UnmanagedType.LPWStr)] string wszProperty, IntPtr ppQualSet);
+
+ [PreserveSig]
+ int Clone([MarshalAs(UnmanagedType.Interface)] out IWbemClassObject ppCopy);
+
+ [PreserveSig]
+ int GetObjectText(int lFlags, [MarshalAs(UnmanagedType.BStr)] out string pstrObjectText);
+
+ [PreserveSig]
+ int SpawnDerivedClass(int lFlags, IntPtr ppNewClass);
+
+ [PreserveSig]
+ int SpawnInstance(int lFlags, IntPtr ppNewInstance);
+
+ [PreserveSig]
+ int CompareTo(int lFlags, IntPtr pCompareTo);
+
+ [PreserveSig]
+ int GetPropertyOrigin([MarshalAs(UnmanagedType.LPWStr)] string wszName, [MarshalAs(UnmanagedType.BStr)] out string pstrClassName);
+
+ [PreserveSig]
+ int InheritsFrom([MarshalAs(UnmanagedType.LPWStr)] string strAncestor);
+
+ [PreserveSig]
+ int GetMethod([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags, IntPtr ppInSignature, IntPtr ppOutSignature);
+
+ [PreserveSig]
+ int PutMethod([MarshalAs(UnmanagedType.LPWStr)] string wszName, int lFlags, IntPtr pInSignature, IntPtr pOutSignature);
+
+ [PreserveSig]
+ int DeleteMethod([MarshalAs(UnmanagedType.LPWStr)] string wszName);
+
+ [PreserveSig]
+ int BeginMethodEnumeration(int lEnumFlags);
+
+ [PreserveSig]
+ int NextMethod(int lFlags, [MarshalAs(UnmanagedType.BStr)] out string pstrName, IntPtr ppInSignature, IntPtr ppOutSignature);
+
+ [PreserveSig]
+ int EndMethodEnumeration();
+
+ [PreserveSig]
+ int GetMethodQualifierSet([MarshalAs(UnmanagedType.LPWStr)] string wszMethod, IntPtr ppQualSet);
+
+ [PreserveSig]
+ int GetMethodOrigin([MarshalAs(UnmanagedType.LPWStr)] string wszMethodName, [MarshalAs(UnmanagedType.BStr)] out string pstrClassName);
+ }
+
+#if !NET
+ ///
+ /// Sets the proxy blanket on a COM proxy via raw interface pointers.
+ /// On .NET Framework, the RCW caches separate proxy stubs for IUnknown and each
+ /// specific COM interface. CoSetProxyBlanket must be called on both the IUnknown
+ /// pointer and the specific interface pointer, otherwise WMI calls fail with
+ /// WBEM_E_ACCESS_DENIED (0x80041003).
+ ///
+ private static void SetProxyBlanketForNetFx(object comProxy)
+ {
+ IntPtr pUnk = Marshal.GetIUnknownForObject(comProxy);
+ try
+ {
+ CoSetProxyBlanketPtr(
+ pUnk,
+ RPC_C_AUTHN_WINNT,
+ RPC_C_AUTHZ_NONE,
+ IntPtr.Zero,
+ RPC_C_AUTHN_LEVEL_CALL,
+ RPC_C_IMP_LEVEL_IMPERSONATE,
+ IntPtr.Zero,
+ EOAC_NONE);
+ }
+ finally
+ {
+ Marshal.Release(pUnk);
+ }
+
+ IntPtr pInterface = Marshal.GetComInterfaceForObject(comProxy, typeof(T));
+ try
+ {
+ CoSetProxyBlanketPtr(
+ pInterface,
+ RPC_C_AUTHN_WINNT,
+ RPC_C_AUTHZ_NONE,
+ IntPtr.Zero,
+ RPC_C_AUTHN_LEVEL_CALL,
+ RPC_C_IMP_LEVEL_IMPERSONATE,
+ IntPtr.Zero,
+ EOAC_NONE);
+ }
+ finally
+ {
+ Marshal.Release(pInterface);
+ }
+ }
+#endif
+
+ ///
+ /// Retrieves the command line for a process by querying WMI Win32_Process via COM.
+ /// Runs: SELECT CommandLine FROM Win32_Process WHERE ProcessId=''
+ ///
+ internal static string? GetCommandLine(int processId)
+ {
+ int hr = CoInitializeSecurity(
+ IntPtr.Zero,
+ -1,
+ IntPtr.Zero,
+ IntPtr.Zero,
+ RPC_C_AUTHN_LEVEL_DEFAULT,
+ RPC_C_IMP_LEVEL_IMPERSONATE,
+ IntPtr.Zero,
+ EOAC_NONE,
+ IntPtr.Zero);
+ // RPC_E_TOO_LATE (0x80010119) means another call already set security — not fatal.
+ if (hr != WBEM_S_NO_ERROR && hr != RPC_E_TOO_LATE)
+ {
+ throw new InvalidOperationException(
+ $"WMI CoInitializeSecurity failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+ Guid clsid = CLSID_WbemLocator;
+ Guid iid = IID_IWbemLocator;
+ hr = CoCreateInstance(ref clsid, IntPtr.Zero, CLSCTX_INPROC_SERVER, ref iid, out IWbemLocator locator);
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI CoCreateInstance failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+ hr = locator.ConnectServer(
+ @"ROOT\CIMV2",
+ strUser: null, strPassword: null, strLocale: null,
+ lSecurityFlags: 0, strAuthority: null,
+ pCtx: IntPtr.Zero,
+ out IWbemServices services);
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI ConnectServer failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+#if NET
+ hr = CoSetProxyBlanket(
+ services,
+ RPC_C_AUTHN_WINNT,
+ RPC_C_AUTHZ_NONE,
+ IntPtr.Zero,
+ RPC_C_AUTHN_LEVEL_CALL,
+ RPC_C_IMP_LEVEL_IMPERSONATE,
+ IntPtr.Zero,
+ EOAC_NONE);
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI CoSetProxyBlanket failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+#else
+ // On .NET Framework, the RCW caches separate proxy stubs for IUnknown and
+ // each specific COM interface. CoSetProxyBlanket must be called on BOTH
+ // the IUnknown pointer AND the specific interface pointer, because the
+ // blanket set on IUnknown doesn't propagate to the specific interface's proxy.
+ SetProxyBlanketForNetFx(services);
+#endif
+
+ string query = $"SELECT CommandLine FROM Win32_Process WHERE ProcessId='{processId}'";
+ hr = services.ExecQuery(
+ "WQL",
+ query,
+ WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
+ null,
+ out IEnumWbemClassObject enumerator);
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI ExecQuery failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+#if !NET
+ // The enumerator is a separate COM proxy that also needs its security blanket set.
+ SetProxyBlanketForNetFx(enumerator);
+#endif
+
+ hr = enumerator.Next(WBEM_INFINITE, 1, out IWbemClassObject obj, out uint returned);
+ if (hr == WBEM_S_FALSE || returned == 0)
+ {
+ // No matching process found.
+ return null;
+ }
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI IEnumWbemClassObject.Next failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+ object val = null!;
+ hr = obj.Get("CommandLine", 0, ref val, IntPtr.Zero, IntPtr.Zero);
+ if (hr != WBEM_S_NO_ERROR)
+ {
+ throw new InvalidOperationException(
+ $"WMI IWbemClassObject.Get(\"CommandLine\") failed for PID {processId}. HRESULT: 0x{hr:X8}");
+ }
+
+ return val as string;
+ }
+ }
+
+#if NET
+ ///
+ /// Linux-specific command line retrieval via /proc/{pid}/cmdline.
+ ///
+ [SupportedOSPlatform("linux")]
+ private static class Linux
+ {
+ ///
+ /// Reads /proc/{pid}/cmdline where arguments are null-byte separated,
+ /// and joins them with spaces.
+ ///
+ internal static string? GetCommandLine(int processId)
+ {
+ try
+ {
+ string cmdlinePath = $"/proc/{processId}/cmdline";
+ byte[] cmdlineBytes = File.ReadAllBytes(cmdlinePath);
+ if (cmdlineBytes.Length == 0)
+ {
+ return null;
+ }
+
+ return ParseNullSeparatedArguments(cmdlineBytes);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+
+ ///
+ /// macOS/BSD-specific P/Invoke bindings and command line retrieval via sysctl KERN_PROCARGS2.
+ ///
+ [SupportedOSPlatform("macos")]
+ [SupportedOSPlatform("freebsd")]
+ private static partial class BSD
+ {
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int sysctl(
+ ReadOnlySpan name,
+ uint namelen,
+ Span oldp,
+ ref nuint oldlenp,
+ ReadOnlySpan newp,
+ nuint newlen);
+
+ ///
+ /// Wrapper over the raw sysctl P/Invoke that is optimized for reading values, not writing.
+ ///
+ private static int Sysctl(ReadOnlySpan name, Span oldp, ref nuint oldlenp)
+ => sysctl(name, (uint)name.Length, oldp, ref oldlenp, ReadOnlySpan.Empty, 0);
+
+ private const int CTL_KERN = 1;
+ private const int KERN_PROCARGS2 = 49;
+
+ ///
+ /// Uses sysctl with KERN_PROCARGS2 to read the process arguments,
+ /// then parses the null-separated buffer using span-based slicing with ArrayPool for efficient memory management.
+ /// Related: https://github.com/dotnet/runtime/issues/101837
+ ///
+ internal static string? GetCommandLine(int processId)
+ {
+ ReadOnlySpan mib = [CTL_KERN, KERN_PROCARGS2, processId];
+ nuint size = 0;
+
+ // Get the required buffer size
+ if (Sysctl(mib, Span.Empty, ref size) != 0 || size == 0)
+ {
+ return null;
+ }
+
+ // Rent a buffer from ArrayPool and pin it for sysctl
+ byte[] buffer = ArrayPool.Shared.Rent((int)size);
+ try
+ {
+ if (Sysctl(mib, buffer.AsSpan(0, (int)size), ref size) != 0)
+ {
+ return null;
+ }
+
+ // Buffer format (KERN_PROCARGS2):
+ // int argc (number of arguments including executable)
+ // fully-qualified executable path (null-terminated)
+ // padding null bytes
+ // argv[0] .. argv[argc-1] (each null-terminated)
+ // environment variables (not needed)
+ ReadOnlySpan data = buffer.AsSpan(0, (int)size);
+
+ if (data.Length < sizeof(int))
+ {
+ return null;
+ }
+
+ int argc = MemoryMarshal.Read(data);
+ if (argc <= 0)
+ {
+ return null;
+ }
+
+ data = data.Slice(sizeof(int));
+
+ // Skip past the executable path (first null terminator)
+ int execPathEnd = data.IndexOf((byte)0);
+ if (execPathEnd < 0)
+ {
+ return null;
+ }
+
+ data = data.Slice(execPathEnd + 1);
+
+ // Skip padding null bytes between executable path and argv[0]
+ while (!data.IsEmpty && data[0] == 0)
+ {
+ data = data.Slice(1);
+ }
+
+ return ParseNullSeparatedArguments(data, argc);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+ }
+#endif
}
}
diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
index 81cb05cc550..0ae65d5be14 100644
--- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
+++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
@@ -52,7 +52,6 @@
-
diff --git a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
index e92f6c7fffd..8f52ca1a0dd 100644
--- a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
+++ b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
@@ -6,6 +6,7 @@
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;
+using Xunit.Abstractions;
#nullable disable
@@ -13,6 +14,22 @@ namespace Microsoft.Build.UnitTests
{
public class ProcessExtensions_Tests
{
+ private readonly ITestOutputHelper _output;
+
+ public ProcessExtensions_Tests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ private static Process StartLongRunningProcess()
+ {
+ var psi = NativeMethodsShared.IsWindows
+ ? new ProcessStartInfo("ping", "-n 31 127.0.0.1")
+ : new ProcessStartInfo("sleep", "30");
+ psi.UseShellExecute = false;
+ return Process.Start(psi);
+ }
+
[Fact]
public async Task KillTree()
{
@@ -32,5 +49,66 @@ public async Task KillTree()
p.HasExited.ShouldBe(true);
p.ExitCode.ShouldNotBe(0);
}
+
+ [Fact]
+ public async Task TryGetCommandLine_RunningProcess_ContainsExpectedExecutable()
+ {
+ using Process p = StartLongRunningProcess();
+ try
+ {
+ await Task.Delay(300);
+ var sw = Stopwatch.StartNew();
+ p.TryGetCommandLine(out string commandLine).ShouldBeTrue();
+ sw.Stop();
+ _output.WriteLine($"TryGetCommandLine elapsed: {sw.Elapsed.TotalMilliseconds:F2} ms");
+
+ if (NativeMethodsShared.IsWindows)
+ {
+ commandLine.ShouldContain("ping", Case.Insensitive);
+ }
+ else
+ {
+ commandLine.ShouldContain("sleep");
+ }
+ }
+ finally
+ {
+ if (!p.HasExited)
+ {
+ p.KillTree(5000);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task TryGetCommandLine_RunningProcess_ContainsArguments()
+ {
+ using Process p = StartLongRunningProcess();
+ try
+ {
+ await Task.Delay(300);
+ var sw = Stopwatch.StartNew();
+ p.TryGetCommandLine(out string commandLine);
+ sw.Stop();
+ _output.WriteLine($"TryGetCommandLine elapsed: {sw.Elapsed.TotalMilliseconds:F2} ms");
+
+ if (NativeMethodsShared.IsWindows)
+ {
+ // ping -n 31 127.0.0.1 – at minimum "127.0.0.1" or "31" should appear
+ commandLine.ShouldMatch(@"(127\.0\.0\.1|31)");
+ }
+ else
+ {
+ commandLine.ShouldContain("30");
+ }
+ }
+ finally
+ {
+ if (!p.HasExited)
+ {
+ p.KillTree(5000);
+ }
+ }
+ }
}
}