Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e72e177
Add cross-platform process command line retrieval and improved node d…
Copilot Feb 12, 2026
c34b0ab
Fix build errors - add nullable disable and internal test methods
Copilot Feb 12, 2026
e949c96
Address code review feedback - fix encoding and race conditions
Copilot Feb 12, 2026
7d8fbbd
Improve code clarity with better variable names
Copilot Feb 12, 2026
7230071
Implement Windows command line retrieval for .NET Core and use regex …
Copilot Feb 12, 2026
4fb31d2
Simplify UNICODE_STRING buffer offset calculation with clear comments
Copilot Feb 13, 2026
11fd084
Add macOS support for process command line retrieval using sysctl
Copilot Feb 13, 2026
17d593b
Remove NodeMode_Tests.cs per review feedback
Copilot Feb 13, 2026
e4cbd03
Enable nullable, fix resource leaks, optimize regex, and remove trivi…
Copilot Feb 13, 2026
7660837
Add macOS-specific test and OSXOnlyFact attribute, remove Process dis…
Copilot Feb 13, 2026
230482e
Remove dotnet process expansion and use P/Invoke for all Windows builds
Copilot Feb 13, 2026
0f5f861
Add MSBuild.dll filtering for dotnet processes
Copilot Feb 13, 2026
1421cf7
Fix macOS KERN_PROCARGS2 buffer parsing to skip padding nulls
Copilot Feb 17, 2026
efe0d8c
refactor native invocations a bit using LibraryImport instead of DllI…
baronfel Feb 17, 2026
eb7e443
Address review feedback: regex source gen, MacOSOnlyFact, change wave…
Copilot Feb 17, 2026
0751a1b
add span-based nodemode parser method
baronfel Feb 17, 2026
1c3bc0e
add string nodemode detection to regex, make it generated, and share …
baronfel Feb 17, 2026
d4d0290
minimize diffs
baronfel Feb 17, 2026
ae5368a
Create Wave18_6 and use it for node discovery feature
Copilot Feb 17, 2026
3835e43
Fix build error and optimize regex: add missing semicolon, extract pa…
Copilot Feb 17, 2026
9730a54
Add comprehensive trace logging for node discovery filtering
Copilot Feb 17, 2026
f9ae292
Support fallback on BSD systems
baronfel Feb 17, 2026
dfa51d7
Return to basics because I am bad at low-level marshalling
baronfel Feb 18, 2026
95da05c
support checking for target process architecture
baronfel Feb 18, 2026
e29eb81
wmi for windows
baronfel Feb 20, 2026
81f56b8
Consolidate tests and add timing information
baronfel Feb 20, 2026
d393ffb
remove useless test
baronfel Feb 20, 2026
f63f6ab
Minor cleanup
baronfel Feb 20, 2026
5da478b
Merge branch 'main' into copilot/improve-node-discovery-for-msbuild
baronfel Feb 20, 2026
7ea20a6
Fix unsupported OS handling to return false instead of null command line
Copilot Feb 23, 2026
fde4df9
Fix both the Windows test scenarios AND the COM code for .NET Framework
baronfel Feb 23, 2026
de6063e
Improve macOS/BSD sysctl implementation with ArrayPool and better spa…
Copilot Feb 23, 2026
8d03bab
Address feedback from @AR-May
baronfel Feb 24, 2026
374c8ee
Re-add KillTree test per review feedback
Copilot Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
<Compile Include="..\Shared\EscapingUtilities.cs" />
<Compile Include="..\Shared\FileUtilitiesRegex.cs" />
<Compile Include="..\Shared\ExceptionHandling.cs" />
<Compile Include="..\Shared\ProcessExtensions.cs" />
<None Include="..\Shared\UnitTests\App.config">
<Link>App.config</Link>
<SubType>Designer</SubType>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Microsoft.Build.BackEnd
/// Contains the shared pieces of code from NodeProviderOutOfProc
/// and NodeProviderOutOfProcTaskHost.
/// </summary>
internal abstract class NodeProviderOutOfProcBase
internal abstract partial class NodeProviderOutOfProcBase
{
/// <summary>
/// The maximum number of bytes to write
Expand Down Expand Up @@ -218,6 +218,10 @@ protected IList<NodeContext> 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
Expand All @@ -229,7 +233,7 @@ protected IList<NodeContext> GetNodes(
if (nodeReuseRequested)
{
IList<Process> possibleRunningNodesList;
(expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation);
(expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation, expectedNodeMode);
possibleRunningNodes = new ConcurrentQueue<Process>(possibleRunningNodesList);

if (possibleRunningNodesList.Count > 0)
Expand Down Expand Up @@ -395,25 +399,102 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
}

/// <summary>
/// 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.
/// </summary>
/// <param name="msbuildLocation"></param>
/// <param name="msbuildLocation">The location of the MSBuild executable</param>
/// <param name="expectedNodeMode">The NodeMode to filter for, or null to include all</param>
/// <returns>
/// 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.
/// </returns>
private (string expectedProcessName, IList<Process> nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null)
private (string expectedProcessName, IList<Process> nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null, NodeMode? expectedNodeMode = null)
{
if (String.IsNullOrEmpty(msbuildLocation))
{
msbuildLocation = Constants.MSBuildExecutableName;
}

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<Process>());
}

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<Process> 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() ?? "<null>", 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

<ItemGroup>
<Compile Include="..\Shared\EscapingUtilities.cs" />
<Compile Include="..\Shared\ProcessExtensions.cs" />
<Compile Include="..\Shared\ResourceUtilities.cs" />
<Compile Include="..\Shared\FileUtilitiesRegex.cs" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/Framework/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ internal static bool IsLinux
/// <summary>
/// Gets a flag indicating if we are running under flavor of BSD (NetBSD, OpenBSD, FreeBSD)
/// </summary>
[SupportedOSPlatformGuard("freebsd")]
internal static bool IsBSD
{
#if CLR2COMPATIBILITY
Expand Down Expand Up @@ -813,6 +814,7 @@ internal static bool IsWindows
/// <summary>
/// Gets a flag indicating if we are running under Mac OSX
/// </summary>
[SupportedOSPlatformGuard("macos")]
internal static bool IsOSX
{
#if CLR2COMPATIBILITY
Expand Down
79 changes: 77 additions & 2 deletions src/Framework/NodeMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal enum NodeMode
/// <summary>
/// Helper methods for the NodeMode enum.
/// </summary>
internal static class NodeModeHelper
internal static partial class NodeModeHelper
{
/// <summary>
/// Converts a NodeMode value to a command line argument string.
Expand All @@ -53,12 +53,45 @@ internal static class NodeModeHelper
/// <returns>True if parsing succeeded, false otherwise</returns>
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
/// <summary>
/// Tries to parse a node mode value from a span, supporting both integer values and enum names (case-insensitive).
/// </summary>
/// <param name="value">The value to parse (can be an integer or enum name)</param>
/// <param name="nodeMode">The parsed NodeMode value if successful</param>
/// <returns>True if parsing succeeded, false otherwise</returns>
public static bool TryParse(ReadOnlySpan<char> value, [NotNullWhen(true)] out NodeMode? nodeMode)
{
return TryParseImpl(value, out nodeMode);
}
#endif

#if NET
private static bool TryParseImpl(ReadOnlySpan<char> 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))
Expand All @@ -69,7 +102,7 @@ public static bool TryParse(string value, [NotNullWhen(true)] out NodeMode? node
nodeMode = (NodeMode)intValue;
return true;
}

return false;
}

Expand All @@ -82,5 +115,47 @@ public static bool TryParse(string value, [NotNullWhen(true)] out NodeMode? node

return false;
}

/// <summary>
/// Extracts the NodeMode from a command line string using regex pattern matching.
/// </summary>
/// <param name="commandLine">The command line to parse. Note that this can't be a span because generated regex don't have a Span Match overload</param>
/// <returns>The NodeMode if found, otherwise null</returns>
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:(?<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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks>$(RuntimeOutputTargetFrameworks)</TargetFrameworks>
<PlatformTarget>$(RuntimeOutputPlatformTarget)</PlatformTarget>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
29 changes: 1 addition & 28 deletions src/Shared/Debugging/DebugUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -57,32 +55,7 @@ internal static void SetDebugPath()
}

private static readonly Lazy<NodeMode?> ProcessNodeMode = new(
() =>
{
return ScanNodeMode(Environment.CommandLine);

NodeMode? ScanNodeMode(string input)
{
var match = Regex.Match(input, @"/nodemode:(?<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()
{
Expand Down
Loading
Loading