| File: Coordinator\CoordinatorSettings.cs | Web Access |
| Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework) |
// 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.Security.Cryptography; using System.Text; using Microsoft.Build.BackEnd; #if NET using Microsoft.Build.Utilities; #endif namespace Microsoft.Build.Framework.Coordinator; /// <summary> /// Shared settings for coordinator client and server behavior. /// </summary> internal sealed record class CoordinatorSettings() { public const string PipeNameBase = "msbuild-coordinator"; public const int DefaultHeartbeatIntervalMs = 5_000; public const int DefaultMissedHeartbeatsThreshold = 3; public const int DefaultInitialConnectionTimeoutMs = 200; public const int DefaultConnectionTimeoutMs = 5_000; public const int DefaultShutdownTimeoutMs = 60_000; public const int MaxHeartbeatIntervalMs = 300_000; private static string DefaultPipeName => $"{PipeNameBase}-{EnvironmentUtilities.CurrentUserName}"; private int? _heartbeatIntervalMs; private int? _missedHeartbeatsThreshold; private int? _initialConnectionTimeoutMs; private int? _connectionTimeoutMs; private int? _shutdownTimeoutMs; private int? _totalNodeBudget; private int? _processId; /// <summary> /// Singleton settings instance populated with default values. /// </summary> public static CoordinatorSettings Default { get; } = new(); public string PipeName { get => field ??= NamedPipeUtil.GetPlatformSpecificPipeName(DefaultPipeName); init => field = NamedPipeUtil.GetPlatformSpecificPipeName(value); } public int HeartbeatIntervalMs { get => _heartbeatIntervalMs ??= DefaultHeartbeatIntervalMs; init => _heartbeatIntervalMs = value > 0 ? Math.Min(value, MaxHeartbeatIntervalMs) : DefaultHeartbeatIntervalMs; } public int MissedHeartbeatsThreshold { get => _missedHeartbeatsThreshold ??= DefaultMissedHeartbeatsThreshold; init => _missedHeartbeatsThreshold = value > 0 ? value : DefaultMissedHeartbeatsThreshold; } public int TotalNodeBudget { get => _totalNodeBudget ??= Environment.ProcessorCount; init => _totalNodeBudget = value <= 0 ? Environment.ProcessorCount : value; } public int ShutdownTimeoutMs { get => _shutdownTimeoutMs ??= DefaultShutdownTimeoutMs; init => _shutdownTimeoutMs = value >= 0 ? value : DefaultShutdownTimeoutMs; } /// <summary> /// The timeout in milliseconds for the initial fast probe to detect an already-running coordinator. /// </summary> public int InitialConnectionTimeoutMs { get => _initialConnectionTimeoutMs ??= DefaultInitialConnectionTimeoutMs; init => _initialConnectionTimeoutMs = value > 0 ? value : DefaultInitialConnectionTimeoutMs; } public int ConnectionTimeoutMs { get => _connectionTimeoutMs ??= DefaultConnectionTimeoutMs; init => _connectionTimeoutMs = value > 0 ? value : DefaultConnectionTimeoutMs; } public int ProcessId { get => _processId ??= EnvironmentUtilities.CurrentProcessId; init => _processId = value > 0 ? value : EnvironmentUtilities.CurrentProcessId; } /// <summary> /// Computes the heartbeat timeout in milliseconds using long arithmetic to avoid overflow. /// </summary> public long HeartbeatTimeoutMs => (long)HeartbeatIntervalMs * MissedHeartbeatsThreshold; /// <summary> /// The named mutex used by the coordinator server to ensure single-instance execution. /// </summary> public string ServerMutexName => GetMutexName("server"); /// <summary> /// The named mutex used by clients to serialize coordinator launch attempts. /// </summary> public string LaunchMutexName => GetMutexName("launch"); public static CoordinatorSettings FromEnvironment() { string? pipeNameOverride = Environment.GetEnvironmentVariable(Constants.PipeNameEnvVarName); string pipeName = !string.IsNullOrEmpty(pipeNameOverride) ? pipeNameOverride : DefaultPipeName; return Default with { PipeName = pipeName, HeartbeatIntervalMs = EnvironmentUtilities.GetValueAsInt32OrDefault( Constants.HeartbeatIntervalEnvVarName, DefaultHeartbeatIntervalMs), MissedHeartbeatsThreshold = DefaultMissedHeartbeatsThreshold, TotalNodeBudget = EnvironmentUtilities.GetValueAsInt32OrDefault( Constants.NodeBudgetEnvVarName, Environment.ProcessorCount), ShutdownTimeoutMs = EnvironmentUtilities.GetValueAsInt32OrDefault( Constants.ShutdownTimeoutEnvVarName, DefaultShutdownTimeoutMs), ConnectionTimeoutMs = DefaultConnectionTimeoutMs, ProcessId = EnvironmentUtilities.CurrentProcessId, }; } /// <summary> /// Generates a platform-appropriate mutex name by combining the pipe name with a purpose suffix. /// </summary> private string GetMutexName(string purpose) { if (NativeMethods.IsWindows) { return $"Global\\{PipeName}-{purpose}"; } // Named mutexes on Unix do not accept path-like names (for example '/tmp/...'). // Hash the pipe name into a stable, compact identifier safe for the runtime. string prefix = $"msbuild-coordinator-{purpose}-"; #if NET int byteCount = Encoding.UTF8.GetByteCount(PipeName); using BufferScope<byte> pipeNameBytes = byteCount <= 256 ? new(stackalloc byte[byteCount]) : new(byteCount); Encoding.UTF8.GetBytes(PipeName, pipeNameBytes); Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes]; SHA256.HashData(pipeNameBytes[..byteCount], hash); #else using SHA256 sha256 = SHA256.Create(); byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(PipeName)); #endif // We're being a bit clever here by defining PrefixAndHash as a ref struct on modern .NET // since string.Create<TState> is defined with a 'where TState : allows ref struct' constraint. return string.Create(prefix.Length + (hash.Length * 2), new PrefixAndHash(prefix, hash), static (span, state) => { var (prefix, hash) = state; prefix.CopyTo(span); span = span[prefix.Length..]; for (int i = 0; i < hash.Length; i++) { byte b = hash[i]; span[0] = HexDigitChar(b / 16); span[1] = HexDigitChar(b % 16); span = span[2..]; } }); static char HexDigitChar(int value) => (char)(value + (value < 10 ? '0' : 'a' - 10)); } #if NET private readonly ref struct PrefixAndHash { public readonly ReadOnlySpan<char> Prefix; public readonly ReadOnlySpan<byte> Hash; public PrefixAndHash(ReadOnlySpan<char> prefix, ReadOnlySpan<byte> hash) { Prefix = prefix; Hash = hash; } public void Deconstruct(out ReadOnlySpan<char> prefix, out ReadOnlySpan<byte> hash) { prefix = Prefix; hash = Hash; } } #else private readonly struct PrefixAndHash(string prefix, byte[] hash) { public ReadOnlySpan<char> Prefix => prefix; public ReadOnlySpan<byte> Hash => hash; public void Deconstruct(out ReadOnlySpan<char> prefix, out ReadOnlySpan<byte> hash) { prefix = Prefix; hash = Hash; } } #endif }