File: src\libraries\Common\src\Interop\Linux\cgroups\Interop.cgroups.cs
Web Access
Project: src\src\libraries\System.Diagnostics.Process\src\System.Diagnostics.Process.csproj (System.Diagnostics.Process)
// 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.Buffers.Text;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
 
internal static partial class Interop
{
    /// <summary>Provides access to some cgroup (v1 and v2) features</summary>
    internal static partial class @cgroups
    {
        // For cgroup v1, see https://www.kernel.org/doc/Documentation/cgroup-v1/
        // For cgroup v2, see https://www.kernel.org/doc/Documentation/cgroup-v2.txt
        // For disambiguation, see https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups-
 
        /// <summary>The supported versions of cgroup.</summary>
        internal enum CGroupVersion
        {
            None,
            CGroup1,
            CGroup2
        };
 
        /// <summary>Path to cgroup filesystem that tells us which version of cgroup is in use.</summary>
        private const string SysFsCgroupFileSystemPath = "/sys/fs/cgroup";
        /// <summary>Path to mountinfo file in procfs for the current process.</summary>
        private const string ProcMountInfoFilePath = "/proc/self/mountinfo";
        /// <summary>Path to cgroup directory in procfs for the current process.</summary>
        private const string ProcCGroupFilePath = "/proc/self/cgroup";
 
        /// <summary>The version of cgroup that's being used. Mutated by tests only.</summary>
        internal static readonly CGroupVersion s_cgroupVersion = FindCGroupVersion();
 
        /// <summary>Path to the found cgroup memory hierarchy mount path, or null if it couldn't be found.</summary>
        internal static readonly string? s_cgroupMemoryHierarchyMountPath = FindCGroupMemoryHierarchyMountPath(s_cgroupVersion);
 
        /// <summary>Path to the found cgroup memory limit path, or null if it couldn't be found.</summary>
        internal static readonly string? s_cgroupMemoryPath = FindCGroupMemoryPath(s_cgroupVersion);
 
        /// <summary>Tries to read the memory limit from the cgroup memory location.</summary>
        /// <param name="limit">The read limit, or 0 if it couldn't be read.</param>
        /// <returns>true if the limit was read successfully; otherwise, false.</returns>
        public static bool TryGetMemoryLimit(out ulong limit)
        {
            if (s_cgroupVersion == CGroupVersion.CGroup1)
            {
                return TryGetMemoryLimitV1(out limit);
            }
            else if (s_cgroupVersion == CGroupVersion.CGroup2)
            {
                return TryGetMemoryLimitV2(out limit);
            }
 
            limit = 0;
            return false;
        }
 
        /// <summary>Tries to read a field of a specified name from the memory.stat file in the current cgroup (cgroup v1 only).</summary>
        /// <param name="fieldName">Name of the field to read.</param>
        /// <param name="val">Value of the field or 0 if the field was not found.</param>
        /// <returns>true if the field was read successfully; otherwise, false.</returns>
        internal static bool TryGetMemoryStatField(string fieldName, out ulong val)
        {
            string? path = s_cgroupMemoryPath;
            if (path != null)
            {
                try
                {
                    // Each field name in the memory.stat is separated by one space from its value
                    fieldName += ' ';
                    foreach (string line in File.ReadLines(path + "/memory.stat"))
                    {
                        if (line.StartsWith(fieldName))
                        {
                            bool foundFieldValue = ulong.TryParse(line.AsSpan(fieldName.Length), out val);
                            return foundFieldValue;
                        }
                    }
                }
                catch (Exception e)
                {
                    Debug.Fail($"Failed to read \"{path}/memory.stat\": {e}");
                }
            }
 
            val = 0;
            return false;
        }
 
        /// <summary>Tries to read the memory limit from the cgroup v1 hierarchy.</summary>
        /// <param name="limit">The read limit, or 0 if it couldn't be read.</param>
        /// <returns>true if the limit was read successfully; otherwise, false.</returns>
        internal static bool TryGetMemoryLimitV1(out ulong limit)
        {
            string? path = s_cgroupMemoryPath;
            if (path != null)
            {
                if (TryReadMemoryValueFromFile(path + "/memory.use_hierarchy", out ulong useHierarchy) && (useHierarchy != 0))
                {
                    return TryGetMemoryStatField("hierarchical_memory_limit", out limit);
                }
 
                if (path != null &&
                    TryReadMemoryValueFromFile(path + "/memory.limit_in_bytes", out limit))
                {
                    return true;
                }
            }
 
            limit = 0;
            return false;
        }
 
        /// <summary>Tries to read the memory limit from the cgroup v2 hierarchy.</summary>
        /// <param name="limit">The read limit, or 0 if it couldn't be read.</param>
        /// <returns>true if the limit was read successfully; otherwise, false.</returns>
        internal static bool TryGetMemoryLimitV2(out ulong limit)
        {
            bool foundAnyLimit = false;
            ulong minLimit = ulong.MaxValue;
            string? currentCGroupMemoryPath = s_cgroupMemoryPath;
            string? cgroupMemoryHierarchyMountPath = s_cgroupMemoryHierarchyMountPath;
            if (currentCGroupMemoryPath != null && cgroupMemoryHierarchyMountPath != null)
            {
                // Iterate over the directory hierarchy representing the cgroup hierarchy until reaching the
                // mount directory. The mount directory doesn't contain the memory.max.
                do
                {
                    if (TryReadMemoryValueFromFile(currentCGroupMemoryPath + "/memory.max", out ulong currentLevelLimit))
                    {
                        foundAnyLimit = true;
                        if (currentLevelLimit < minLimit)
                        {
                            minLimit = currentLevelLimit;
                        }
                    }
                    currentCGroupMemoryPath = Path.GetDirectoryName(currentCGroupMemoryPath);
                }
                while (currentCGroupMemoryPath!.Length != cgroupMemoryHierarchyMountPath.Length);
            }
 
            limit = minLimit;
 
            return foundAnyLimit;
        }
 
        /// <summary>Tries to parse a memory limit from the specified file.</summary>
        /// <param name="path">The path to the file to parse.</param>
        /// <param name="result">The parsed result, or 0 if it couldn't be parsed.</param>
        /// <returns>true if the value was read successfully; otherwise, false.</returns>
        internal static bool TryReadMemoryValueFromFile(string path, out ulong result)
        {
            if (File.Exists(path))
            {
                try
                {
                    byte[] bytes = File.ReadAllBytes(path);
                    if (Utf8Parser.TryParse(bytes, out ulong ulongValue, out int bytesConsumed))
                    {
                        // If we successfully parsed the number, see if there's a K, M, or G
                        // multiplier value immediately following.
                        ulong multiplier = 1;
                        if (bytesConsumed < bytes.Length)
                        {
                            switch (bytes[bytesConsumed])
                            {
 
                                case (byte)'k':
                                case (byte)'K':
                                    multiplier = 1024;
                                    break;
 
                                case (byte)'m':
                                case (byte)'M':
                                    multiplier = 1024 * 1024;
                                    break;
 
                                case (byte)'g':
                                case (byte)'G':
                                    multiplier = 1024 * 1024 * 1024;
                                    break;
                            }
                        }
 
                        result = checked(ulongValue * multiplier);
                        return true;
                    }
 
                    // 'max' is also a possible valid value
                    //
                    // Treat this as 'no memory limit' and let the caller
                    // fallback to reading the real limit via other means
                }
                catch (Exception e)
                {
                    Debug.Fail($"Failed to read \"{path}\": {e}");
                }
            }
 
            result = 0;
            return false;
        }
 
        /// <summary>Find the cgroup version in use on the system.</summary>
        /// <returns>The cgroup version.</returns>
        private static unsafe CGroupVersion FindCGroupVersion()
        {
            CGroupVersion cgroupVersion = CGroupVersion.None;
            const int MountPointFormatBufferSizeInBytes = 32;
            byte* formatBuffer = stackalloc byte[MountPointFormatBufferSizeInBytes];    // format names should be small
            long numericFormat;
            int result = Interop.Sys.GetFormatInfoForMountPoint(SysFsCgroupFileSystemPath, formatBuffer, MountPointFormatBufferSizeInBytes, &numericFormat);
            if (result == 0)
            {
                if (numericFormat == (int)Interop.Sys.UnixFileSystemTypes.cgroup2fs)
                {
                    cgroupVersion = CGroupVersion.CGroup2;
                }
                else
                {
                    // Assume that if /sys/fs/cgroup exists and the file system type is not cgroup2fs,
                    // it is cgroup v1. Typically the file system type is tmpfs, but other values have
                    // been seen in the wild.
                    cgroupVersion = CGroupVersion.CGroup1;
                }
            }
 
            return cgroupVersion;
        }
 
        private static string? FindCGroupMemoryHierarchyMountPath(CGroupVersion cgroupVersion)
        {
            if (TryFindHierarchyMount(cgroupVersion, "memory", out string? _, out string? hierarchyMount))
            {
                return hierarchyMount;
            }
 
            return null;
        }
 
        /// <summary>Find the cgroup memory.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <returns>The limit path if found; otherwise, null.</returns>
        private static string? FindCGroupMemoryPath(CGroupVersion cgroupVersion)
        {
            return FindCGroupPath(cgroupVersion, "memory");
        }
 
        /// <summary>Find the cgroup path for the specified subsystem.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <param name="subsystem">The subsystem, e.g. "memory".</param>
        /// <returns>The cgroup path if found; otherwise, null.</returns>
        private static string? FindCGroupPath(CGroupVersion cgroupVersion, string subsystem)
        {
            if (cgroupVersion == CGroupVersion.None)
            {
                return null;
            }
 
            if (TryFindHierarchyMount(cgroupVersion, subsystem, out string? hierarchyRoot, out string? hierarchyMount) &&
                TryFindCGroupPathForSubsystem(cgroupVersion, subsystem, out string? cgroupPathRelativeToMount))
            {
                return FindCGroupPath(hierarchyRoot, hierarchyMount, cgroupPathRelativeToMount);
            }
 
            return null;
        }
 
        internal static string FindCGroupPath(string hierarchyRoot, string hierarchyMount, string cgroupPathRelativeToMount)
        {
            // For a host cgroup, we need to append the relative path.
            // The root and cgroup path can share a common prefix of the path that should not be appended.
            // Example 1 (docker):
            // hierarchyMount:               /sys/fs/cgroup/cpu
            // hierarchyRoot:                /docker/87ee2de57e51bc75175a4d2e81b71d162811b179d549d6601ed70b58cad83578
            // cgroupPathRelativeToMount:    /docker/87ee2de57e51bc75175a4d2e81b71d162811b179d549d6601ed70b58cad83578/my_named_cgroup
            // append to the cgroupPath:     /my_named_cgroup
            // final cgroupPath:             /sys/fs/cgroup/cpu/my_named_cgroup
            //
            // Example 2 (out of docker)
            // hierarchyMount:               /sys/fs/cgroup/cpu
            // hierarchyRoot:                /
            // cgroupPathRelativeToMount:    /my_named_cgroup
            // append to the cgroupPath:     /my_named_cgroup
            // final cgroupPath:             /sys/fs/cgroup/cpu/my_named_cgroup
 
            int commonPathPrefixLength = hierarchyRoot.Length;
            if ((commonPathPrefixLength == 1) || !cgroupPathRelativeToMount.StartsWith(hierarchyRoot, StringComparison.Ordinal))
            {
                commonPathPrefixLength = 0;
            }
 
            return string.Concat(hierarchyMount, cgroupPathRelativeToMount.AsSpan(commonPathPrefixLength));
        }
 
        /// <summary>Find the cgroup mount information for the specified subsystem.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <param name="subsystem">The subsystem, e.g. "memory".</param>
        /// <param name="root">The path of the directory in the filesystem which forms the root of this mount; null if not found.</param>
        /// <param name="path">The path of the mount point relative to the process's root directory; null if not found.</param>
        /// <returns>true if the mount was found; otherwise, null.</returns>
        private static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path)
        {
            return TryFindHierarchyMount(cgroupVersion, ProcMountInfoFilePath, subsystem, out root, out path);
        }
 
        /// <summary>Find the cgroup mount information for the specified subsystem.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <param name="mountInfoFilePath">The path to the /mountinfo file. Useful for tests.</param>
        /// <param name="subsystem">The subsystem, e.g. "memory".</param>
        /// <param name="root">The path of the directory in the filesystem which forms the root of this mount; null if not found.</param>
        /// <param name="path">The path of the mount point relative to the process's root directory; null if not found.</param>
        /// <returns>true if the mount was found; otherwise, null.</returns>
        internal static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string mountInfoFilePath, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path)
        {
            if (File.Exists(mountInfoFilePath))
            {
                try
                {
                    using (var reader = new StreamReader(mountInfoFilePath))
                    {
                        string? line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            // Look for an entry that has cgroup as the "filesystem type"
                            // and, for cgroup1, that has options containing the specified subsystem
                            // See man page for /proc/[pid]/mountinfo for details, e.g.:
                            //     (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
                            //     36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
                            // but (7) is optional and could exist as multiple fields; the (8) separator marks
                            // the end of the optional values.
 
                            const string Separator = " - ";
                            int endOfOptionalFields = line.IndexOf(Separator, StringComparison.Ordinal);
                            if (endOfOptionalFields == -1)
                            {
                                // Malformed line.
                                continue;
                            }
 
                            string postSeparatorLine = line.Substring(endOfOptionalFields + Separator.Length);
                            string[] postSeparatorlineParts = postSeparatorLine.Split(' ');
                            if (postSeparatorlineParts.Length < 3)
                            {
                                // Malformed line.
                                continue;
                            }
 
                            if (cgroupVersion == CGroupVersion.CGroup1)
                            {
                                bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") &&
                                        (Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0));
                                if (!validCGroup1Entry)
                                {
                                    continue;
                                }
                            }
                            else if (cgroupVersion == CGroupVersion.CGroup2)
                            {
                                bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2";
                                if (!validCGroup2Entry)
                                {
                                    continue;
                                }
 
                            }
                            else
                            {
                                Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\"");
                            }
 
 
                            string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
                            root = lineParts[3];
                            path = lineParts[4];
 
                            return true;
                        }
                    }
                }
                catch (Exception e)
                {
                    Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
                }
            }
 
            root = null;
            path = null;
            return false;
        }
 
        /// <summary>Find the cgroup relative path for the specified subsystem.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <param name="subsystem">The subsystem, e.g. "memory".</param>
        /// <param name="path">The found path, or null if it couldn't be found.</param>
        /// <returns>true if a cgroup path for the subsystem is found.</returns>
        private static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? path)
        {
            return TryFindCGroupPathForSubsystem(cgroupVersion, ProcCGroupFilePath, subsystem, out path);
        }
 
        /// <summary>Find the cgroup relative path for the specified subsystem.</summary>
        /// <param name="cgroupVersion">The cgroup version currently in use on the system.</param>
        /// <param name="procCGroupFilePath">Path to cgroup directory in procfs for the current process.</param>
        /// <param name="subsystem">The subsystem, e.g. "memory".</param>
        /// <param name="path">The found path, or null if it couldn't be found.</param>
        /// <returns>true if a cgroup path for the subsystem is found.</returns>
        internal static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, string procCGroupFilePath, string subsystem, [NotNullWhen(true)] out string? path)
        {
            if (File.Exists(procCGroupFilePath))
            {
                try
                {
                    using (var reader = new StreamReader(procCGroupFilePath))
                    {
                        Span<Range> lineParts = stackalloc Range[4];
                        string? line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            ReadOnlySpan<char> lineSpan = line;
                            if (lineSpan.Split(lineParts, ':') != 3)
                            {
                                // Malformed line.
                                continue;
                            }
 
                            if (cgroupVersion == CGroupVersion.CGroup1)
                            {
                                // cgroup v1: Find the first entry that has the subsystem listed in its controller
                                // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g:
                                //     hierarchy-ID:controller-list:cgroup-path
                                //     5:cpuacct,cpu,cpuset:/daemons
                                if (Array.IndexOf(line[lineParts[1]].Split(','), subsystem) < 0)
                                {
                                    // Not the relevant entry.
                                    continue;
                                }
 
                                path = line[lineParts[2]];
                                return true;
                            }
                            else if (cgroupVersion == CGroupVersion.CGroup2)
                            {
                                // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy:
                                //     0::$PATH
 
                                if (lineSpan[lineParts[0]] is "0" && lineSpan[lineParts[1]].IsEmpty)
                                {
                                    path = line[lineParts[2]];
                                    return true;
                                }
                            }
                            else
                            {
                                Debug.Fail($"Unexpected cgroup version: \"{cgroupVersion}\"");
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    Debug.Fail($"Failed to read or parse \"{procCGroupFilePath}\": {e}");
                }
            }
 
            path = null;
            return false;
        }
    }
}