From 2baef4d953b0a09c996db2daeca7ac25481e00e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 2 Mar 2026 12:33:23 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20"Improve=20cross-platform=20node=20dis?= =?UTF-8?q?covery=20for=20reuse=20with=20NodeMode=20filteri=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c48fb46ac1a2cc22ed2605b58ea04e3347a27b9f. --- ...Microsoft.Build.Engine.OM.UnitTests.csproj | 1 + .../NodeProviderOutOfProcBase.cs | 99 +-- ...Microsoft.Build.Framework.UnitTests.csproj | 1 + src/Framework/NativeMethods.cs | 2 - src/Framework/NodeMode.cs | 79 +- ...crosoft.Build.CommandLine.UnitTests.csproj | 1 - src/Shared/Debugging/DebugUtils.cs | 29 +- src/Shared/ProcessExtensions.cs | 812 +----------------- .../Microsoft.Build.Tasks.UnitTests.csproj | 1 + .../ProcessExtensions_Tests.cs | 78 -- 10 files changed, 48 insertions(+), 1055 deletions(-) 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); - } - } - } } }