392 lines
20 KiB
Diff
392 lines
20 KiB
Diff
|
From 2b2273ea4ea1c28472fa0d6ad2ffeb6374500550 Mon Sep 17 00:00:00 2001
|
|||
|
From: Omair Majid <omajid@redhat.com>
|
|||
|
Date: Wed, 23 Oct 2019 17:45:59 -0400
|
|||
|
Subject: [PATCH 1/2] Add cgroup v2 support to Interop.cgroups
|
|||
|
|
|||
|
Fix up code to adjust cgroup v1 assumptions and check cgroup v2 paths,
|
|||
|
locations and values.
|
|||
|
|
|||
|
Continue using the older cgroup v1 terminology for APIs.
|
|||
|
---
|
|||
|
.../Interop/Linux/cgroups/Interop.cgroups.cs | 116 ++++++++++++++----
|
|||
|
src/Common/tests/Common.Tests.csproj | 4 +
|
|||
|
.../tests/Tests/Interop/cgroupsTests.cs | 107 ++++++++++++++++
|
|||
|
.../tests/DescriptionNameTests.cs | 2 +-
|
|||
|
4 files changed, 206 insertions(+), 23 deletions(-)
|
|||
|
create mode 100644 src/Common/tests/Tests/Interop/cgroupsTests.cs
|
|||
|
|
|||
|
diff --git a/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
|
|||
|
index 0ffd4d7b7c03..186fe0516c5b 100644
|
|||
|
--- a/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
|
|||
|
+++ b/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
|
|||
|
@@ -9,17 +9,22 @@
|
|||
|
|
|||
|
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
|
|||
|
+
|
|||
|
+ /// <summary>The version of cgroup that's being used </summary>
|
|||
|
+ internal enum CGroupVersion { None, CGroup1, CGroup2 };
|
|||
|
+
|
|||
|
/// <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>Path to the found cgroup location, or null if it couldn't be found.</summary>
|
|||
|
- internal static readonly string s_cgroupMemoryPath = FindCGroupPath("memory");
|
|||
|
- /// <summary>Path to the found cgroup memory limit_in_bytes path, or null if it couldn't be found.</summary>
|
|||
|
- private static readonly string s_cgroupMemoryLimitPath = s_cgroupMemoryPath != null ? s_cgroupMemoryPath + "/memory.limit_in_bytes" : null;
|
|||
|
+ /// <summary>Path to the found cgroup memory limit path, or null if it couldn't be found.</summary>
|
|||
|
+ internal static readonly string s_cgroupMemoryLimitPath = FindCGroupMemoryLimitPath();
|
|||
|
|
|||
|
/// <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>
|
|||
|
@@ -42,7 +47,7 @@ public static bool TryGetMemoryLimit(out ulong limit)
|
|||
|
/// <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>
|
|||
|
- private static bool TryReadMemoryValueFromFile(string path, out ulong result)
|
|||
|
+ internal static bool TryReadMemoryValueFromFile(string path, out ulong result)
|
|||
|
{
|
|||
|
if (File.Exists(path))
|
|||
|
{
|
|||
|
@@ -79,6 +84,11 @@ private static bool TryReadMemoryValueFromFile(string path, out ulong result)
|
|||
|
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)
|
|||
|
{
|
|||
|
@@ -90,12 +100,35 @@ private static bool TryReadMemoryValueFromFile(string path, out ulong result)
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
+ /// <summary>Find the cgroup memory limit path.</summary>
|
|||
|
+ /// <returns>The limit path if found; otherwise, null.</returns>
|
|||
|
+ private static string FindCGroupMemoryLimitPath()
|
|||
|
+ {
|
|||
|
+ string cgroupMemoryPath = FindCGroupPath("memory", out CGroupVersion version);
|
|||
|
+ if (cgroupMemoryPath != null)
|
|||
|
+ {
|
|||
|
+ if (version == CGroupVersion.CGroup1)
|
|||
|
+ {
|
|||
|
+ return cgroupMemoryPath + "/memory.limit_in_bytes";
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ if (version == CGroupVersion.CGroup2)
|
|||
|
+ {
|
|||
|
+ // 'memory.high' is a soft limit; the process may get throttled
|
|||
|
+ // 'memory.max' is where OOM killer kicks in
|
|||
|
+ return cgroupMemoryPath + "/memory.max";
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ return null;
|
|||
|
+ }
|
|||
|
+
|
|||
|
/// <summary>Find the cgroup path for the specified subsystem.</summary>
|
|||
|
/// <param name="subsystem">The subsystem, e.g. "memory".</param>
|
|||
|
/// <returns>The cgroup path if found; otherwise, null.</returns>
|
|||
|
- private static string FindCGroupPath(string subsystem)
|
|||
|
+ private static string FindCGroupPath(string subsystem, out CGroupVersion version)
|
|||
|
{
|
|||
|
- if (TryFindHierarchyMount(subsystem, out string hierarchyRoot, out string hierarchyMount) &&
|
|||
|
+ if (TryFindHierarchyMount(subsystem, out version, out string hierarchyRoot, out string hierarchyMount) &&
|
|||
|
TryFindCGroupPathForSubsystem(subsystem, out string cgroupPathRelativeToMount))
|
|||
|
{
|
|||
|
// For a host cgroup, we need to append the relative path.
|
|||
|
@@ -113,19 +146,24 @@ private static string FindCGroupPath(string subsystem)
|
|||
|
/// <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(string subsystem, out string root, out string path)
|
|||
|
+ private static bool TryFindHierarchyMount(string subsystem, out CGroupVersion version, out string root, out string path)
|
|||
|
{
|
|||
|
- if (File.Exists(ProcMountInfoFilePath))
|
|||
|
+ return TryFindHierarchyMount(ProcMountInfoFilePath, subsystem, out version, out root, out path);
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ internal static bool TryFindHierarchyMount(string mountInfoFilePath, string subsystem, out CGroupVersion version, out string root, out string path)
|
|||
|
+ {
|
|||
|
+ if (File.Exists(mountInfoFilePath))
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
- using (var reader = new StreamReader(ProcMountInfoFilePath))
|
|||
|
+ 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 that has options containing the specified subsystem.
|
|||
|
+ // 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
|
|||
|
@@ -148,17 +186,35 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
- if (postSeparatorlineParts[0] != "cgroup" ||
|
|||
|
- Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) < 0)
|
|||
|
+ bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") &&
|
|||
|
+ (Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0));
|
|||
|
+ bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2";
|
|||
|
+
|
|||
|
+ if (!validCGroup1Entry && !validCGroup2Entry)
|
|||
|
{
|
|||
|
// Not the relevant entry.
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
- // Found the relevant entry. Extract the mount root and path.
|
|||
|
+ // Found the relevant entry. Extract the cgroup version, mount root and path.
|
|||
|
+ switch (postSeparatorlineParts[0])
|
|||
|
+ {
|
|||
|
+ case "cgroup":
|
|||
|
+ version = CGroupVersion.CGroup1;
|
|||
|
+ break;
|
|||
|
+ case "cgroup2":
|
|||
|
+ version = CGroupVersion.CGroup2;
|
|||
|
+ break;
|
|||
|
+ default:
|
|||
|
+ version = CGroupVersion.None;
|
|||
|
+ Debug.Fail($"invalid value for CGroupVersion \"{postSeparatorlineParts[0]}\"");
|
|||
|
+ break;
|
|||
|
+ }
|
|||
|
+
|
|||
|
string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
|
|||
|
root = lineParts[3];
|
|||
|
path = lineParts[4];
|
|||
|
+
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
@@ -169,6 +225,7 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
+ version = CGroupVersion.None;
|
|||
|
root = null;
|
|||
|
path = null;
|
|||
|
return false;
|
|||
|
@@ -180,27 +237,42 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
|
|||
|
/// <returns></returns>
|
|||
|
private static bool TryFindCGroupPathForSubsystem(string subsystem, out string path)
|
|||
|
{
|
|||
|
- if (File.Exists(ProcCGroupFilePath))
|
|||
|
+ return TryFindCGroupPathForSubsystem(ProcCGroupFilePath, subsystem, out path);
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ internal static bool TryFindCGroupPathForSubsystem(string procCGroupFilePath, string subsystem, out string path)
|
|||
|
+ {
|
|||
|
+ if (File.Exists(procCGroupFilePath))
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
- using (var reader = new StreamReader(ProcCGroupFilePath))
|
|||
|
+ using (var reader = new StreamReader(procCGroupFilePath))
|
|||
|
{
|
|||
|
string line;
|
|||
|
while ((line = reader.ReadLine()) != null)
|
|||
|
{
|
|||
|
- // 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
|
|||
|
-
|
|||
|
string[] lineParts = line.Split(':');
|
|||
|
+
|
|||
|
if (lineParts.Length != 3)
|
|||
|
{
|
|||
|
// Malformed line.
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
+ // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy:
|
|||
|
+ // 0::$PATH
|
|||
|
+
|
|||
|
+ if ((lineParts[0] == "0") && (string.Empty == lineParts[1]))
|
|||
|
+ {
|
|||
|
+ path = lineParts[2];
|
|||
|
+ return true;
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ // 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(lineParts[1].Split(','), subsystem) < 0)
|
|||
|
{
|
|||
|
// Not the relevant entry.
|
|||
|
@@ -214,7 +286,7 @@ private static bool TryFindCGroupPathForSubsystem(string subsystem, out string p
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
- Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
|
|||
|
+ Debug.Fail($"Failed to read or parse \"{procCGroupFilePath}\": {e}");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
diff --git a/src/Common/tests/Common.Tests.csproj b/src/Common/tests/Common.Tests.csproj
|
|||
|
index a189d856348b..979c8dd7fbe6 100644
|
|||
|
--- a/src/Common/tests/Common.Tests.csproj
|
|||
|
+++ b/src/Common/tests/Common.Tests.csproj
|
|||
|
@@ -12,6 +12,9 @@
|
|||
|
<Compile Include="$(CommonTestPath)\System\Security\Cryptography\ByteUtils.cs">
|
|||
|
<Link>Common\System\Security\Cryptography\ByteUtils.cs</Link>
|
|||
|
</Compile>
|
|||
|
+ <Compile Include="$(CommonPath)\Interop\Linux\cgroups\Interop.cgroups.cs">
|
|||
|
+ <Link>Common\Interop\Linux\cgroups\Interop.cgroups.cs</Link>
|
|||
|
+ </Compile>
|
|||
|
<Compile Include="$(CommonPath)\Interop\Linux\procfs\Interop.ProcFsStat.cs">
|
|||
|
<Link>Common\Interop\Linux\procfs\Interop.ProcFsStat.cs</Link>
|
|||
|
</Compile>
|
|||
|
@@ -69,6 +72,7 @@
|
|||
|
<Compile Include="$(CommonPath)\CoreLib\System\PasteArguments.cs">
|
|||
|
<Link>Common\CoreLib\System\PasteArguments.cs</Link>
|
|||
|
</Compile>
|
|||
|
+ <Compile Include="Tests\Interop\cgroupsTests.cs" />
|
|||
|
<Compile Include="Tests\Interop\procfsTests.cs" />
|
|||
|
<Compile Include="Tests\System\CharArrayHelpersTests.cs" />
|
|||
|
<Compile Include="Tests\System\IO\PathInternal.Tests.cs" />
|
|||
|
diff --git a/src/Common/tests/Tests/Interop/cgroupsTests.cs b/src/Common/tests/Tests/Interop/cgroupsTests.cs
|
|||
|
new file mode 100644
|
|||
|
index 000000000000..f16d9242879c
|
|||
|
--- /dev/null
|
|||
|
+++ b/src/Common/tests/Tests/Interop/cgroupsTests.cs
|
|||
|
@@ -0,0 +1,107 @@
|
|||
|
+// Licensed to the .NET Foundation under one or more agreements.
|
|||
|
+// The .NET Foundation licenses this file to you under the MIT license.
|
|||
|
+// See the LICENSE file in the project root for more information.
|
|||
|
+
|
|||
|
+using System;
|
|||
|
+using System.IO;
|
|||
|
+using System.Text;
|
|||
|
+using Xunit;
|
|||
|
+
|
|||
|
+namespace Common.Tests
|
|||
|
+{
|
|||
|
+ public class cgroupsTests
|
|||
|
+ {
|
|||
|
+ [Theory]
|
|||
|
+ [InlineData(true, "0", 0)]
|
|||
|
+ [InlineData(false, "max", 0)]
|
|||
|
+ [InlineData(true, "1k", 1024)]
|
|||
|
+ [InlineData(true, "1K", 1024)]
|
|||
|
+ public static void ValidateTryReadMemoryValue(bool expectedResult, string valueText, ulong expectedValue)
|
|||
|
+ {
|
|||
|
+ string path = Path.GetTempFileName();
|
|||
|
+ try
|
|||
|
+ {
|
|||
|
+ File.WriteAllText(path, valueText);
|
|||
|
+
|
|||
|
+ bool result = Interop.cgroups.TryReadMemoryValueFromFile(path, out ulong val);
|
|||
|
+
|
|||
|
+ Assert.Equal(expectedResult, result);
|
|||
|
+ if (result)
|
|||
|
+ {
|
|||
|
+ Assert.Equal(expectedValue, val);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+ finally
|
|||
|
+ {
|
|||
|
+ File.Delete(path);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ [Theory]
|
|||
|
+ [InlineData(false, "0 0 0:0 / /foo ignore ignore - overlay overlay ignore", "ignore", 0, "/", "/")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "memory", 2, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo-with-dashes")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo-with-dashes")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu,memory", "memory", 1, "/", "/foo")]
|
|||
|
+ [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory,cpu", "memory", 1, "/", "/foo")]
|
|||
|
+ [InlineData(false, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu", "memory", 0, "/", "/foo")]
|
|||
|
+ public static void ParseValidateMountInfo(bool found, string procSelfMountInfoText, string subsystem, int expectedVersion, string expectedRoot, string expectedMount)
|
|||
|
+ {
|
|||
|
+ string path = Path.GetTempFileName();
|
|||
|
+ try
|
|||
|
+ {
|
|||
|
+ File.WriteAllText(path, procSelfMountInfoText);
|
|||
|
+
|
|||
|
+ bool result = Interop.cgroups.TryFindHierarchyMount(path, subsystem, out Interop.cgroups.CGroupVersion version, out string root, out string mount);
|
|||
|
+
|
|||
|
+ Assert.Equal(found, result);
|
|||
|
+ if (found)
|
|||
|
+ {
|
|||
|
+ Assert.Equal(expectedVersion, (int)version);
|
|||
|
+ Assert.Equal(expectedRoot, root);
|
|||
|
+ Assert.Equal(expectedMount, mount);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+ finally
|
|||
|
+ {
|
|||
|
+ File.Delete(path);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+
|
|||
|
+ [Theory]
|
|||
|
+ [InlineData(true, "0::/foo", "ignore", "/foo")]
|
|||
|
+ [InlineData(true, "0::/bar", "ignore", "/bar")]
|
|||
|
+ [InlineData(true, "0::frob", "ignore", "frob")]
|
|||
|
+ [InlineData(false, "1::frob", "ignore", "ignore")]
|
|||
|
+ [InlineData(true, "1:foo:bar", "foo", "bar")]
|
|||
|
+ [InlineData(true, "2:foo:bar", "foo", "bar")]
|
|||
|
+ [InlineData(false, "2:foo:bar", "bar", "ignore")]
|
|||
|
+ [InlineData(true, "1:foo:bar\n2:eggs:spam", "foo", "bar")]
|
|||
|
+ [InlineData(true, "1:foo:bar\n2:eggs:spam", "eggs", "spam")]
|
|||
|
+ public static void ParseValidateProcCGroup(bool found, string procSelfCgroupText, string subsystem, string expectedMountPath)
|
|||
|
+ {
|
|||
|
+ string path = Path.GetTempFileName();
|
|||
|
+ try
|
|||
|
+ {
|
|||
|
+ File.WriteAllText(path, procSelfCgroupText);
|
|||
|
+
|
|||
|
+ bool result = Interop.cgroups.TryFindCGroupPathForSubsystem(path, subsystem, out string mountPath);
|
|||
|
+
|
|||
|
+ Assert.Equal(found, result);
|
|||
|
+ if (found)
|
|||
|
+ {
|
|||
|
+ Assert.Equal(expectedMountPath, mountPath);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+ finally
|
|||
|
+ {
|
|||
|
+ File.Delete(path);
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+ }
|
|||
|
+}
|
|||
|
diff --git a/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs b/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
|
|||
|
index 910af2fd82b4..73f692898dbc 100644
|
|||
|
--- a/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
|
|||
|
+++ b/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
|
|||
|
@@ -40,7 +40,7 @@ public void DumpRuntimeInformationToConsole()
|
|||
|
|
|||
|
Console.WriteLine($"### CURRENT DIRECTORY: {Environment.CurrentDirectory}");
|
|||
|
|
|||
|
- string cgroupsLocation = Interop.cgroups.s_cgroupMemoryPath;
|
|||
|
+ string cgroupsLocation = Interop.cgroups.s_cgroupMemoryLimitPath;
|
|||
|
if (cgroupsLocation != null)
|
|||
|
{
|
|||
|
Console.WriteLine($"### CGROUPS MEMORY: {cgroupsLocation}");
|
|||
|
|