diff --git a/src/libraries/Common/src/Interop/Linux/os-release/Interop.OSReleaseFile.cs b/src/libraries/Common/src/Interop/Linux/os-release/Interop.OSReleaseFile.cs
new file mode 100644
index 00000000000000..7f16a6b4426deb
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Linux/os-release/Interop.OSReleaseFile.cs
@@ -0,0 +1,83 @@
+// 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.IO;
+
+internal static partial class Interop
+{
+ // Parse information from '/etc/os-release'.
+ internal static class OSReleaseFile
+ {
+ private const string EtcOsReleasePath = "/etc/os-release";
+
+ ///
+ /// Returns a user-friendly distribution name.
+ ///
+ internal static string? GetPrettyName(string filename = EtcOsReleasePath)
+ {
+ if (File.Exists(filename))
+ {
+ string[] lines;
+ try
+ {
+ lines = File.ReadAllLines(filename);
+ }
+ catch
+ {
+ return null;
+ }
+
+ // Parse the NAME, PRETTY_NAME, and VERSION fields.
+ // These fields are suitable for presentation to the user.
+ ReadOnlySpan prettyName = default, name = default, version = default;
+ foreach (string line in lines)
+ {
+ ReadOnlySpan lineSpan = line.AsSpan();
+
+ _ = TryGetFieldValue(lineSpan, "PRETTY_NAME=", ref prettyName) ||
+ TryGetFieldValue(lineSpan, "NAME=", ref name) ||
+ TryGetFieldValue(lineSpan, "VERSION=", ref version);
+
+ // Prefer "PRETTY_NAME".
+ if (!prettyName.IsEmpty)
+ {
+ return new string(prettyName);
+ }
+ }
+
+ // Fall back to "NAME[ VERSION]".
+ if (!name.IsEmpty)
+ {
+ if (!version.IsEmpty)
+ {
+ return string.Concat(name, " ", version);
+ }
+ return new string(name);
+ }
+
+ static bool TryGetFieldValue(ReadOnlySpan line, ReadOnlySpan prefix, ref ReadOnlySpan value)
+ {
+ if (!line.StartsWith(prefix))
+ {
+ return false;
+ }
+ ReadOnlySpan fieldValue = line.Slice(prefix.Length);
+
+ // Remove enclosing quotes.
+ if (fieldValue.Length >= 2 &&
+ fieldValue[0] is '"' or '\'' &&
+ fieldValue[0] == fieldValue[^1])
+ {
+ fieldValue = fieldValue[1..^1];
+ }
+
+ value = fieldValue;
+ return true;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/libraries/Common/tests/Common.Tests.csproj b/src/libraries/Common/tests/Common.Tests.csproj
index 5ed2a1ea9b5eae..0c7fe898431ee8 100644
--- a/src/libraries/Common/tests/Common.Tests.csproj
+++ b/src/libraries/Common/tests/Common.Tests.csproj
@@ -16,6 +16,8 @@
Link="Common\Interop\Linux\procfs\Interop.ProcFsStat.cs" />
+
+
diff --git a/src/libraries/Common/tests/Tests/Interop/OSReleaseTests.cs b/src/libraries/Common/tests/Tests/Interop/OSReleaseTests.cs
new file mode 100644
index 00000000000000..0dc6528c85d4ad
--- /dev/null
+++ b/src/libraries/Common/tests/Tests/Interop/OSReleaseTests.cs
@@ -0,0 +1,61 @@
+// 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.IO;
+using System.Text;
+using Xunit;
+
+namespace Common.Tests
+{
+ public class OSReleaseTests : FileCleanupTestBase
+ {
+ [Theory]
+ // Double quotes:
+ [InlineData("NAME=\"Fedora\"\nVERSION=\"37\"\nPRETTY_NAME=\"Fedora Linux 37\"", "Fedora Linux 37")]
+ [InlineData("NAME=\"Fedora\"\nVERSION=\"37\"", "Fedora 37")]
+ [InlineData("NAME=\"Fedora\"", "Fedora")]
+ // Single quotes:
+ [InlineData("NAME='Ubuntu'\nVERSION='22.04'\nPRETTY_NAME='Ubuntu Linux 22.04'", "Ubuntu Linux 22.04")]
+ [InlineData("NAME='Ubuntu'\nVERSION='22.04'", "Ubuntu 22.04")]
+ [InlineData("NAME='Ubuntu'", "Ubuntu")]
+ // No quotes:
+ [InlineData("NAME=Alpine\nVERSION=3.14\nPRETTY_NAME=Alpine_Linux_3.14", "Alpine_Linux_3.14")]
+ [InlineData("NAME=Alpine\nVERSION=3.14", "Alpine 3.14")]
+ [InlineData("NAME=Alpine", "Alpine")]
+ // No pretty name fields:
+ [InlineData("ID=fedora\nVERSION_ID=37", null)]
+ [InlineData("", null)]
+ public void GetPrettyName_Success(
+ string content,
+ string? expectedName)
+ {
+ string path = GetTestFilePath();
+ File.WriteAllText(path, content);
+
+ string? name = Interop.OSReleaseFile.GetPrettyName(path);
+ Assert.Equal(expectedName, name);
+ }
+
+ [Fact]
+ public void GetPrettyName_NoFile_ReturnsNull()
+ {
+ string path = Path.GetRandomFileName();
+ Assert.False(File.Exists(path));
+
+ string? name = Interop.OSReleaseFile.GetPrettyName(path);
+ Assert.Null(name);
+ }
+
+ [Fact, PlatformSpecific(TestPlatforms.Linux)]
+ public void GetPrettyName_CannotRead_ReturnsNull()
+ {
+ string path = CreateTestFile();
+ File.SetUnixFileMode(path, UnixFileMode.None);
+ Assert.ThrowsAny(() => File.ReadAllText(path));
+
+ string? name = Interop.OSReleaseFile.GetPrettyName(path);
+ Assert.Null(name);
+ }
+ }
+}
diff --git a/src/libraries/Common/tests/Tests/Interop/procfsTests.cs b/src/libraries/Common/tests/Tests/Interop/procfsTests.cs
index c585be7ea14baf..c0c2d1431dc27c 100644
--- a/src/libraries/Common/tests/Tests/Interop/procfsTests.cs
+++ b/src/libraries/Common/tests/Tests/Interop/procfsTests.cs
@@ -7,7 +7,7 @@
namespace Common.Tests
{
- public class procfsTests
+ public class procfsTests : FileCleanupTestBase
{
[Theory]
[InlineData("1 (systemd) S 0 1 1 0 -1 4194560 11536 2160404 55 593 70 169 4213 1622 20 0 1 0 4 189767680 1491 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 4 0 0 25 0 0 0 0 0 0 0 0 0 0", 1, "systemd", 'S', 1, 70, 169, 0, 4, 189767680, 1491, 18446744073709551615)]
@@ -33,33 +33,29 @@ public class procfsTests
[InlineData("5955 (a(((b) S 1806 5955 5955 34823 5955 4194304 1426 5872 0 3 16 3 16 4 20 0 1 0 674762 32677888 1447 18446744073709551615 4194304 5192652 140725672538992 140725672534152 140236068968880 0 0 3670020 1266777851 1 0 0 17 4 0 0 0 0 0 7290352 7326856 21204992 140725672540419 140725672540424 140725672540424 140725672542190 0", 5955, "a(((b", 'S', 5955, 16, 3, 0, 674762, 32677888, 1447, 18446744073709551615)]
[InlineData("5955 (a)( ) b (() () S 1806 5955 5955 34823 5955 4194304 1426 5872 0 3 16 3 16 4 20 0 1 0 674762 32677888 1447 18446744073709551615 4194304 5192652 140725672538992 140725672534152 140236068968880 0 0 3670020 1266777851 1 0 0 17 4 0 0 0 0 0 7290352 7326856 21204992 140725672540419 140725672540424 140725672540424 140725672542190 0", 5955, "a)( ) b (() (", 'S', 5955, 16, 3, 0, 674762, 32677888, 1447, 18446744073709551615)]
[InlineData("5955 (has\\backslash) S 1806 5955 5955 34823 5955 4194304 1426 5872 0 3 16 3 16 4 20 0 1 0 674762 32677888 1447 18446744073709551615 4194304 5192652 140725672538992 140725672534152 140236068968880 0 0 3670020 1266777851 1 0 0 17 4 0 0 0 0 0 7290352 7326856 21204992 140725672540419 140725672540424 140725672540424 140725672542190 0", 5955, "has\\backslash", 'S', 5955, 16, 3, 0, 674762, 32677888, 1447, 18446744073709551615)]
- public static void ParseValidStatFiles_Success(
+ public void ParseValidStatFiles_Success(
string statFileText,
int expectedPid, string expectedComm, char expectedState, int expectedSession,
ulong expectedUtime, ulong expectedStime, long expectedNice, ulong expectedStarttime,
ulong expectedVsize, long expectedRss, ulong expectedRsslim)
{
- string path = Path.GetTempFileName();
- try
- {
- File.WriteAllText(path, statFileText);
+ string path = GetTestFilePath();
+ File.WriteAllText(path, statFileText);
- Interop.procfs.ParsedStat result;
- Assert.True(Interop.procfs.TryParseStatFile(path, out result));
+ Interop.procfs.ParsedStat result;
+ Assert.True(Interop.procfs.TryParseStatFile(path, out result));
- Assert.Equal(expectedPid, result.pid);
- Assert.Equal(expectedComm, result.comm);
- Assert.Equal(expectedState, result.state);
- Assert.Equal(expectedSession, result.session);
- Assert.Equal(expectedUtime, result.utime);
- Assert.Equal(expectedStime, result.stime);
- Assert.Equal(expectedNice, result.nice);
- Assert.Equal(expectedStarttime, result.starttime);
- Assert.Equal(expectedVsize, result.vsize);
- Assert.Equal(expectedRss, result.rss);
- Assert.Equal(expectedRsslim, result.rsslim);
- }
- finally { File.Delete(path); }
+ Assert.Equal(expectedPid, result.pid);
+ Assert.Equal(expectedComm, result.comm);
+ Assert.Equal(expectedState, result.state);
+ Assert.Equal(expectedSession, result.session);
+ Assert.Equal(expectedUtime, result.utime);
+ Assert.Equal(expectedStime, result.stime);
+ Assert.Equal(expectedNice, result.nice);
+ Assert.Equal(expectedStarttime, result.starttime);
+ Assert.Equal(expectedVsize, result.vsize);
+ Assert.Equal(expectedRss, result.rss);
+ Assert.Equal(expectedRsslim, result.rsslim);
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index bb3a70a516cfdc..773f375462c518 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -2379,6 +2379,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/RuntimeInformation.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/RuntimeInformation.Unix.cs
index 1dac29e534d114..5a15e296bd60a5 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/RuntimeInformation.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/RuntimeInformation.Unix.cs
@@ -10,7 +10,7 @@ public static partial class RuntimeInformation
private static string? s_osDescription;
private static volatile int s_osArchPlusOne;
- public static string OSDescription => s_osDescription ??= Interop.Sys.GetUnixVersion();
+ public static string OSDescription => s_osDescription ??= (GetPrettyOSDescription() ?? Interop.Sys.GetUnixVersion());
public static Architecture OSArchitecture
{
@@ -30,5 +30,15 @@ public static Architecture OSArchitecture
return (Architecture)osArch;
}
}
+
+ private static string? GetPrettyOSDescription()
+ {
+ if (OperatingSystem.IsLinux())
+ {
+ return Interop.OSReleaseFile.GetPrettyName();
+ }
+
+ return null;
+ }
}
}
diff --git a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
index 3bcf3e24a67839..755874d854ed16 100644
--- a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
+++ b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
@@ -201,7 +201,14 @@ public void VerifyWindowsName()
[Fact, PlatformSpecific(TestPlatforms.Linux)] // Checks Linux name in RuntimeInformation
public void VerifyLinuxName()
{
- Assert.Contains("linux", RuntimeInformation.OSDescription, StringComparison.OrdinalIgnoreCase);
+ if (File.Exists("/etc/os-release"))
+ {
+ Assert.Equal(Interop.OSReleaseFile.GetPrettyName("/etc/os-release"), RuntimeInformation.OSDescription);
+ }
+ else
+ {
+ Assert.Contains("linux", RuntimeInformation.OSDescription, StringComparison.OrdinalIgnoreCase);
+ }
}
[Fact, PlatformSpecific(TestPlatforms.NetBSD)] // Checks NetBSD name in RuntimeInformation
diff --git a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/System.Runtime.InteropServices.RuntimeInformation.Tests.csproj b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/System.Runtime.InteropServices.RuntimeInformation.Tests.csproj
index 9b5b6f3aedd552..44648878b71ccd 100644
--- a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/System.Runtime.InteropServices.RuntimeInformation.Tests.csproj
+++ b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/System.Runtime.InteropServices.RuntimeInformation.Tests.csproj
@@ -10,5 +10,7 @@
+