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 @@ +