Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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,6 +51,7 @@
<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 partial class NodeProviderOutOfProcBase
internal abstract class NodeProviderOutOfProcBase
{
/// <summary>
/// The maximum number of bytes to write
Expand Down Expand Up @@ -218,10 +218,6 @@ 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 @@ -233,7 +229,7 @@ protected IList<NodeContext> GetNodes(
if (nodeReuseRequested)
{
IList<Process> possibleRunningNodesList;
(expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation, expectedNodeMode);
(expectedProcessName, possibleRunningNodesList) = GetPossibleRunningNodes(msbuildLocation);
possibleRunningNodes = new ConcurrentQueue<Process>(possibleRunningNodesList);

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

/// <summary>
/// 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.
/// </summary>
/// <param name="msbuildLocation">The location of the MSBuild executable</param>
/// <param name="expectedNodeMode">The NodeMode to filter for, or null to include all</param>
/// <param name="msbuildLocation"></param>
/// <returns>
/// 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.
Comment thread
JanProvaznik marked this conversation as resolved.
/// </returns>
private (string expectedProcessName, IList<Process> nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null, NodeMode? expectedNodeMode = null)
private (string expectedProcessName, IList<Process> nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = 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>());
}

// 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
var processes = Process.GetProcessesByName(expectedProcessName);
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,6 +29,7 @@

<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: 0 additions & 2 deletions src/Framework/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,6 @@ 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 @@ -814,7 +813,6 @@ 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: 2 additions & 77 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 partial class NodeModeHelper
internal static class NodeModeHelper
{
/// <summary>
/// Converts a NodeMode value to a command line argument string.
Expand All @@ -53,45 +53,12 @@ internal static partial 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 @@ -102,7 +69,7 @@ private static bool TryParseImpl(string value, [NotNullWhen(true)] out NodeMode?
nodeMode = (NodeMode)intValue;
return true;
}

return false;
}

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

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,7 +4,6 @@
<TargetFrameworks>$(RuntimeOutputTargetFrameworks)</TargetFrameworks>
<PlatformTarget>$(RuntimeOutputPlatformTarget)</PlatformTarget>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

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

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

private static readonly Lazy<NodeMode?> ProcessNodeMode = new(
() => NodeModeHelper.ExtractFromCommandLine(Environment.CommandLine));
() =>
{
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;
}
});

private static bool CurrentProcessMatchesDebugName()
{
Expand Down
Loading