File: Utilities\NativeMethods.cs
Web Access
Project: ..\..\..\src\MSBuildTaskHost\MSBuildTaskHost.csproj (MSBuildTaskHost)
// 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.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Win32;
 
namespace Microsoft.Build.TaskHost.Utilities;
 
internal static class NativeMethods
{
    public static bool Is64Bit => IntPtr.Size == 8;
 
    private const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
 
    /// <summary>
    /// Default buffer size to use when dealing with the Windows API.
    /// </summary>
    private const int MAX_PATH = 260;
 
    private const string WindowsFileSystemKeyName = @"SYSTEM\CurrentControlSet\Control\FileSystem";
    private const string LongPathsEnabledValueName = "LongPathsEnabled";
 
    private enum LOGICAL_PROCESSOR_RELATIONSHIP
    {
        RelationProcessorCore,
        RelationNumaNode,
        RelationCache,
        RelationProcessorPackage,
        RelationGroup,
        RelationAll = 0xffff
    }
 
    private struct SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX
    {
        public LOGICAL_PROCESSOR_RELATIONSHIP Relationship;
        public uint Size;
        public PROCESSOR_RELATIONSHIP Processor;
 
        public SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX()
        {
            Relationship = default;
            Size = default;
            Processor = default;
        }
    }
 
    [StructLayout(LayoutKind.Sequential)]
    private unsafe struct PROCESSOR_RELATIONSHIP
    {
        public byte Flags;
        private byte EfficiencyClass;
        private fixed byte Reserved[20];
        public ushort GroupCount;
        public IntPtr GroupInfo;
    }
 
    /// <summary>
    /// Contains information about a file or directory; used by GetFileAttributesEx.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    private struct WIN32_FILE_ATTRIBUTE_DATA
    {
        internal int fileAttributes;
        internal uint ftCreationTimeLow;
        internal uint ftCreationTimeHigh;
        internal uint ftLastAccessTimeLow;
        internal uint ftLastAccessTimeHigh;
        internal uint ftLastWriteTimeLow;
        internal uint ftLastWriteTimeHigh;
        internal uint fileSizeHigh;
        internal uint fileSizeLow;
    }
 
    public static int GetLogicalCoreCount()
    {
        int numberOfCpus = Environment.ProcessorCount;
 
        // .NET on Windows returns a core count limited to the current NUMA node
        //     https://github.com/dotnet/runtime/issues/29686
        // so always double-check it.
        var result = GetLogicalCoreCountOnWindows();
        if (result != -1)
        {
            numberOfCpus = result;
        }
 
        return numberOfCpus;
    }
 
    /// <summary>
    /// Get the exact physical core count on Windows
    /// Useful for getting the exact core count in 32 bits processes,
    /// as Environment.ProcessorCount has a 32-core limit in that case.
    /// https://github.com/dotnet/runtime/blob/221ad5b728f93489655df290c1ea52956ad8f51c/src/libraries/System.Runtime.Extensions/src/System/Environment.Windows.cs#L171-L210
    /// </summary>
    private static unsafe int GetLogicalCoreCountOnWindows()
    {
        uint len = 0;
        const int ERROR_INSUFFICIENT_BUFFER = 122;
 
        if (!GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, IntPtr.Zero, ref len) &&
            Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER)
        {
            // Allocate that much space
            var buffer = new byte[len];
            fixed (byte* bufferPtr = buffer)
            {
                // Call GetLogicalProcessorInformationEx with the allocated buffer
                if (GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, (IntPtr)bufferPtr, ref len))
                {
                    // Walk each SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX in the buffer, where the Size of each dictates how
                    // much space it's consuming.  For each group relation, count the number of active processors in each of its group infos.
                    int processorCount = 0;
                    byte* ptr = bufferPtr;
                    byte* endPtr = bufferPtr + len;
                    while (ptr < endPtr)
                    {
                        var current = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)ptr;
                        if (current->Relationship == LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore)
                        {
                            // Flags is 0 if the core has a single logical proc, LTP_PC_SMT if more than one
                            // for now, assume "more than 1" == 2, as it has historically been for hyperthreading
                            processorCount += (current->Processor.Flags == 0) ? 1 : 2;
                        }
 
                        ptr += current->Size;
                    }
 
                    return processorCount;
                }
            }
        }
 
        return -1;
    }
 
    /// <summary>
    /// Cached value for MaxPath.
    /// </summary>
    private static int? s_maxPath;
 
    /// <summary>
    /// Gets the max path limit of the current OS.
    /// </summary>
    internal static int MaxPath
        => s_maxPath ??= ComputeMaxPath();
 
    private static int ComputeMaxPath()
        => Traits.Instance.EscapeHatches.DisableLongPaths || !LongPathsEnabled()
            ? MAX_PATH
            : int.MaxValue;
 
    private static bool LongPathsEnabled()
    {
        try
        {
            using RegistryKey? fileSystemKey = Registry.LocalMachine.OpenSubKey(WindowsFileSystemKeyName);
 
            if (fileSystemKey is null)
            {
                return false;
            }
 
            int longPathsEnabledValue = (int)fileSystemKey.GetValue(LongPathsEnabledValueName, defaultValue: -1);
 
            return longPathsEnabledValue == 1;
        }
        catch
        {
            return false;
        }
    }
 
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP RelationshipType, IntPtr Buffer, ref uint ReturnedLength);
 
    /// <summary>
    /// Given an error code, converts it to an HRESULT and throws the appropriate exception.
    /// </summary>
    /// <param name="errorCode"></param>
    private static void ThrowExceptionForErrorCode(int errorCode)
    {
        // See ndp\clr\src\bcl\system\io\__error.cs for this code as it appears in the CLR.
 
        // Something really bad went wrong with the call
        // translate the error into an exception
 
        // Convert the errorcode into an HRESULT (See MakeHRFromErrorCode in Win32Native.cs in
        // ndp\clr\src\bcl\microsoft\win32)
        errorCode = unchecked(((int)0x80070000) | errorCode);
 
        // Throw an exception as best we can
        Marshal.ThrowExceptionForHR(errorCode);
    }
 
    internal static unsafe string GetFullPath(string path)
    {
        char* buffer = stackalloc char[MAX_PATH];
        int fullPathLength = GetFullPathWin32(path, MAX_PATH, buffer, IntPtr.Zero);
 
        // if user is using long paths we could need to allocate a larger buffer
        if (fullPathLength > MAX_PATH)
        {
            char* newBuffer = stackalloc char[fullPathLength];
            fullPathLength = GetFullPathWin32(path, fullPathLength, newBuffer, IntPtr.Zero);
 
            buffer = newBuffer;
        }
 
        // Avoid creating new strings unnecessarily
        return AreStringsEqual(buffer, fullPathLength, path) ? path : new string(buffer, startIndex: 0, length: fullPathLength);
    }
 
    private static unsafe int GetFullPathWin32(string target, int bufferLength, char* buffer, IntPtr mustBeZero)
    {
        int pathLength = GetFullPathName(target, bufferLength, buffer, mustBeZero);
        VerifyThrowWin32Result(pathLength);
        return pathLength;
    }
 
    /// <summary>
    /// Compare an unsafe char buffer with a <see cref="System.String"/> to see if their contents are identical.
    /// </summary>
    /// <param name="buffer">The beginning of the char buffer.</param>
    /// <param name="len">The length of the buffer.</param>
    /// <param name="s">The string.</param>
    /// <returns>True only if the contents of <paramref name="s"/> and the first <paramref name="len"/> characters in <paramref name="buffer"/> are identical.</returns>
    private static unsafe bool AreStringsEqual(char* buffer, int len, string s)
    {
        if (len != s.Length)
        {
            return false;
        }
 
        foreach (char ch in s)
        {
            if (ch != *buffer++)
            {
                return false;
            }
        }
 
        return true;
    }
 
    private static void VerifyThrowWin32Result(int result)
    {
        bool isError = result == 0;
        if (isError)
        {
            int code = Marshal.GetLastWin32Error();
            ThrowExceptionForErrorCode(code);
        }
    }
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation);
 
    [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")]
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetCurrentDirectory")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetCurrentDirectoryWindows(string path);
 
    internal static bool SetCurrentDirectory(string path)
        => SetCurrentDirectoryWindows(path);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern unsafe int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero);
 
    public static bool FileExists(string fullPath)
    {
        WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA();
        bool success = GetFileAttributesEx(fullPath, 0, ref data);
        return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
    }
}