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 fc675e1852f..21f20586dbd 100644
--- a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj
+++ b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj
@@ -51,6 +51,7 @@
+
App.config
Designer
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
index b74013ba0f0..66f78b30ec2 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 partial class NodeProviderOutOfProcBase
+ internal abstract class NodeProviderOutOfProcBase
{
///
/// The maximum number of bytes to write
@@ -218,10 +218,6 @@ 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
@@ -233,7 +229,7 @@ protected IList GetNodes(
if (nodeReuseRequested)
{
IList possibleRunningNodesList;
- (expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation, expectedNodeMode);
+ (expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation);
possibleRunningNodes = new ConcurrentQueue(possibleRunningNodesList);
if (possibleRunningNodesList.Count > 0)
@@ -399,17 +395,14 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
}
///
- /// 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.
+ /// Finds processes named after either msbuild or msbuildtaskhost.
///
- /// The location of the MSBuild executable
- /// The NodeMode to filter for, or null to include all
+ ///
///
- /// Item 1 is a descriptive name of the processes being searched for.
- /// Item 2 is the list of matching processes, sorted by ID.
+ /// Item 1 is the name of the process being searched for.
+ /// Item 2 is the ConcurrentQueue of ordered processes themselves.
///
- private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null, NodeMode? expectedNodeMode = null)
+ private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null)
{
if (String.IsNullOrEmpty(msbuildLocation))
{
@@ -417,84 +410,10 @@ 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());
- }
-
- // 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
+ var processes = Process.GetProcessesByName(expectedProcessName);
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 d31ef573b24..aba4ae6c2d2 100644
--- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj
+++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs
index 4ae854ba1b4..e5858135cf0 100644
--- a/src/Framework/NativeMethods.cs
+++ b/src/Framework/NativeMethods.cs
@@ -772,7 +772,6 @@ 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
@@ -814,7 +813,6 @@ 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 87d5d5bdb05..97c60854006 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 partial class NodeModeHelper
+ internal static class NodeModeHelper
{
///
/// Converts a NodeMode value to a command line argument string.
@@ -53,45 +53,12 @@ internal static partial 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))
@@ -102,7 +69,7 @@ private static bool TryParseImpl(string value, [NotNullWhen(true)] out NodeMode?
nodeMode = (NodeMode)intValue;
return true;
}
-
+
return false;
}
@@ -115,47 +82,5 @@ private static bool TryParseImpl(string value, [NotNullWhen(true)] out NodeMode?
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 378a9068711..4ba8bcc482e 100644
--- a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj
+++ b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj
@@ -4,7 +4,6 @@
$(RuntimeOutputTargetFrameworks)
$(RuntimeOutputPlatformTarget)
false
- true
diff --git a/src/Shared/Debugging/DebugUtils.cs b/src/Shared/Debugging/DebugUtils.cs
index e9e8c1d6eee..59aa2ba9837 100644
--- a/src/Shared/Debugging/DebugUtils.cs
+++ b/src/Shared/Debugging/DebugUtils.cs
@@ -2,7 +2,9 @@
// 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;
@@ -83,7 +85,32 @@ internal static void SetDebugPath()
}
private static readonly Lazy ProcessNodeMode = new(
- () => NodeModeHelper.ExtractFromCommandLine(Environment.CommandLine));
+ () =>
+ {
+ 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;
+ }
+ });
private static bool CurrentProcessMatchesDebugName()
{
diff --git a/src/Shared/ProcessExtensions.cs b/src/Shared/ProcessExtensions.cs
index 7ddd8dc6ab6..91addd48495 100644
--- a/src/Shared/ProcessExtensions.cs
+++ b/src/Shared/ProcessExtensions.cs
@@ -1,20 +1,12 @@
// 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 partial class ProcessExtensions
+ internal static class ProcessExtensions
{
public static void KillTree(this Process process, int timeoutMilliseconds)
{
@@ -25,9 +17,10 @@ public static void KillTree(this Process process, int timeoutMilliseconds)
{
try
{
+ // issue the kill command
NativeMethodsShared.KillTree(process.Id);
}
- catch (InvalidOperationException)
+ catch (System.InvalidOperationException)
{
// The process already exited, which is fine,
// just continue.
@@ -35,806 +28,13 @@ public static void KillTree(this Process process, int timeoutMilliseconds)
}
else
{
- throw new NotSupportedException();
+ throw new System.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 dying, 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 dieing, 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 0ae65d5be14..81cb05cc550 100644
--- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
+++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
@@ -52,6 +52,7 @@
+
diff --git a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
index 8f52ca1a0dd..e92f6c7fffd 100644
--- a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
+++ b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs
@@ -6,7 +6,6 @@
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;
-using Xunit.Abstractions;
#nullable disable
@@ -14,22 +13,6 @@ 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()
{
@@ -49,66 +32,5 @@ 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);
- }
- }
- }
}
}