Skip to content

Commit 9672d82

Browse files
tmdsstephentoub
andauthored
OSDescription.Linux: return a user-friendly name based on /etc/os-release. (#83976)
* OSDescription.Linux: return a user-friendly name based on /etc/os-release. * Use FileCleanupTestBase. * Avoid some allocations by using Span. * expectedName can be null. * Apply suggestions from code review Co-authored-by: Stephen Toub <[email protected]> * Break when we encounter PRETTY_NAME. --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 90d0d5c commit 9672d82

File tree

8 files changed

+186
-23
lines changed

8 files changed

+186
-23
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
7+
internal static partial class Interop
8+
{
9+
// Parse information from '/etc/os-release'.
10+
internal static class OSReleaseFile
11+
{
12+
private const string EtcOsReleasePath = "/etc/os-release";
13+
14+
/// <summary>
15+
/// Returns a user-friendly distribution name.
16+
/// </summary>
17+
internal static string? GetPrettyName(string filename = EtcOsReleasePath)
18+
{
19+
if (File.Exists(filename))
20+
{
21+
string[] lines;
22+
try
23+
{
24+
lines = File.ReadAllLines(filename);
25+
}
26+
catch
27+
{
28+
return null;
29+
}
30+
31+
// Parse the NAME, PRETTY_NAME, and VERSION fields.
32+
// These fields are suitable for presentation to the user.
33+
ReadOnlySpan<char> prettyName = default, name = default, version = default;
34+
foreach (string line in lines)
35+
{
36+
ReadOnlySpan<char> lineSpan = line.AsSpan();
37+
38+
_ = TryGetFieldValue(lineSpan, "PRETTY_NAME=", ref prettyName) ||
39+
TryGetFieldValue(lineSpan, "NAME=", ref name) ||
40+
TryGetFieldValue(lineSpan, "VERSION=", ref version);
41+
42+
// Prefer "PRETTY_NAME".
43+
if (!prettyName.IsEmpty)
44+
{
45+
return new string(prettyName);
46+
}
47+
}
48+
49+
// Fall back to "NAME[ VERSION]".
50+
if (!name.IsEmpty)
51+
{
52+
if (!version.IsEmpty)
53+
{
54+
return string.Concat(name, " ", version);
55+
}
56+
return new string(name);
57+
}
58+
59+
static bool TryGetFieldValue(ReadOnlySpan<char> line, ReadOnlySpan<char> prefix, ref ReadOnlySpan<char> value)
60+
{
61+
if (!line.StartsWith(prefix))
62+
{
63+
return false;
64+
}
65+
ReadOnlySpan<char> fieldValue = line.Slice(prefix.Length);
66+
67+
// Remove enclosing quotes.
68+
if (fieldValue.Length >= 2 &&
69+
fieldValue[0] is '"' or '\'' &&
70+
fieldValue[0] == fieldValue[^1])
71+
{
72+
fieldValue = fieldValue[1..^1];
73+
}
74+
75+
value = fieldValue;
76+
return true;
77+
}
78+
}
79+
80+
return null;
81+
}
82+
}
83+
}

src/libraries/Common/tests/Common.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
Link="Common\Interop\Linux\procfs\Interop.ProcFsStat.cs" />
1717
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.TryReadStatusFile.cs"
1818
Link="Common\Interop\Linux\Interop.ProcFsStat.TryReadStatusFile.cs" />
19+
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs"
20+
Link="Common\Interop\Linux\os-release\Interop.OSReleaseFile.cs" />
1921
<Compile Include="$(CommonPath)System\CharArrayHelpers.cs"
2022
Link="Common\System\CharArrayHelpers.cs" />
2123
<Compile Include="$(CommonPath)System\StringExtensions.cs"
@@ -78,6 +80,7 @@
7880
Link="System\PasteArguments.cs" />
7981
<Compile Include="Tests\Interop\cgroupsTests.cs" />
8082
<Compile Include="Tests\Interop\procfsTests.cs" />
83+
<Compile Include="Tests\Interop\OSReleaseTests.cs" />
8184
<Compile Include="Tests\System\IO\PathInternal.Tests.cs" />
8285
<Compile Include="Tests\System\IO\StringParserTests.cs" />
8386
<Compile Include="Tests\System\Net\HttpDateParserTests.cs" />
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Text;
7+
using Xunit;
8+
9+
namespace Common.Tests
10+
{
11+
public class OSReleaseTests : FileCleanupTestBase
12+
{
13+
[Theory]
14+
// Double quotes:
15+
[InlineData("NAME=\"Fedora\"\nVERSION=\"37\"\nPRETTY_NAME=\"Fedora Linux 37\"", "Fedora Linux 37")]
16+
[InlineData("NAME=\"Fedora\"\nVERSION=\"37\"", "Fedora 37")]
17+
[InlineData("NAME=\"Fedora\"", "Fedora")]
18+
// Single quotes:
19+
[InlineData("NAME='Ubuntu'\nVERSION='22.04'\nPRETTY_NAME='Ubuntu Linux 22.04'", "Ubuntu Linux 22.04")]
20+
[InlineData("NAME='Ubuntu'\nVERSION='22.04'", "Ubuntu 22.04")]
21+
[InlineData("NAME='Ubuntu'", "Ubuntu")]
22+
// No quotes:
23+
[InlineData("NAME=Alpine\nVERSION=3.14\nPRETTY_NAME=Alpine_Linux_3.14", "Alpine_Linux_3.14")]
24+
[InlineData("NAME=Alpine\nVERSION=3.14", "Alpine 3.14")]
25+
[InlineData("NAME=Alpine", "Alpine")]
26+
// No pretty name fields:
27+
[InlineData("ID=fedora\nVERSION_ID=37", null)]
28+
[InlineData("", null)]
29+
public void GetPrettyName_Success(
30+
string content,
31+
string? expectedName)
32+
{
33+
string path = GetTestFilePath();
34+
File.WriteAllText(path, content);
35+
36+
string? name = Interop.OSReleaseFile.GetPrettyName(path);
37+
Assert.Equal(expectedName, name);
38+
}
39+
40+
[Fact]
41+
public void GetPrettyName_NoFile_ReturnsNull()
42+
{
43+
string path = Path.GetRandomFileName();
44+
Assert.False(File.Exists(path));
45+
46+
string? name = Interop.OSReleaseFile.GetPrettyName(path);
47+
Assert.Null(name);
48+
}
49+
50+
[Fact, PlatformSpecific(TestPlatforms.Linux)]
51+
public void GetPrettyName_CannotRead_ReturnsNull()
52+
{
53+
string path = CreateTestFile();
54+
File.SetUnixFileMode(path, UnixFileMode.None);
55+
Assert.ThrowsAny<Exception>(() => File.ReadAllText(path));
56+
57+
string? name = Interop.OSReleaseFile.GetPrettyName(path);
58+
Assert.Null(name);
59+
}
60+
}
61+
}

src/libraries/Common/tests/Tests/Interop/procfsTests.cs

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace Common.Tests
99
{
10-
public class procfsTests
10+
public class procfsTests : FileCleanupTestBase
1111
{
1212
[Theory]
1313
[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
3333
[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)]
3434
[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)]
3535
[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)]
36-
public static void ParseValidStatFiles_Success(
36+
public void ParseValidStatFiles_Success(
3737
string statFileText,
3838
int expectedPid, string expectedComm, char expectedState, int expectedSession,
3939
ulong expectedUtime, ulong expectedStime, long expectedNice, ulong expectedStarttime,
4040
ulong expectedVsize, long expectedRss, ulong expectedRsslim)
4141
{
42-
string path = Path.GetTempFileName();
43-
try
44-
{
45-
File.WriteAllText(path, statFileText);
42+
string path = GetTestFilePath();
43+
File.WriteAllText(path, statFileText);
4644

47-
Interop.procfs.ParsedStat result;
48-
Assert.True(Interop.procfs.TryParseStatFile(path, out result));
45+
Interop.procfs.ParsedStat result;
46+
Assert.True(Interop.procfs.TryParseStatFile(path, out result));
4947

50-
Assert.Equal(expectedPid, result.pid);
51-
Assert.Equal(expectedComm, result.comm);
52-
Assert.Equal(expectedState, result.state);
53-
Assert.Equal(expectedSession, result.session);
54-
Assert.Equal(expectedUtime, result.utime);
55-
Assert.Equal(expectedStime, result.stime);
56-
Assert.Equal(expectedNice, result.nice);
57-
Assert.Equal(expectedStarttime, result.starttime);
58-
Assert.Equal(expectedVsize, result.vsize);
59-
Assert.Equal(expectedRss, result.rss);
60-
Assert.Equal(expectedRsslim, result.rsslim);
61-
}
62-
finally { File.Delete(path); }
48+
Assert.Equal(expectedPid, result.pid);
49+
Assert.Equal(expectedComm, result.comm);
50+
Assert.Equal(expectedState, result.state);
51+
Assert.Equal(expectedSession, result.session);
52+
Assert.Equal(expectedUtime, result.utime);
53+
Assert.Equal(expectedStime, result.stime);
54+
Assert.Equal(expectedNice, result.nice);
55+
Assert.Equal(expectedStarttime, result.starttime);
56+
Assert.Equal(expectedVsize, result.vsize);
57+
Assert.Equal(expectedRss, result.rss);
58+
Assert.Equal(expectedRsslim, result.rsslim);
6359
}
6460
}
6561
}

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,7 @@
23862386
<Compile Include="$(CommonPath)System\IO\StringParser.cs" Condition="'$(TargetsLinux)' == 'true'" Link="Common\System\IO\StringParser.cs" />
23872387
<Compile Include="$(CommonPath)Interop\OSX\Interop.libproc.GetProcessInfoById.cs" Condition="'$(TargetsOSX)' == 'true' or '$(TargetsMacCatalyst)' == 'true'" Link="Common\Interop\OSX\Interop.libproc.GetProcessInfoById.cs" />
23882388
<Compile Include="$(CommonPath)Interop\SunOS\procfs\Interop.ProcFsStat.TryReadProcessStatusInfo.cs" Condition="'$(Targetsillumos)' == 'true' or '$(TargetsSolaris)' == 'true'" Link="Common\Interop\SunOS\Interop.ProcFsStat.TryReadProcessStatusInfo.cs" />
2389+
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs" Condition="'$(TargetsBrowser)' != 'true' and '$(TargetsWasi)' != 'true'" />
23892390
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.cs" />
23902391
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.FreeBSD.cs" Condition="'$(TargetsFreeBSD)' == 'true'" />
23912392
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Linux.cs" Condition="'$(TargetsLinux)' == 'true'" />

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/RuntimeInformation.Unix.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public static partial class RuntimeInformation
1010
private static string? s_osDescription;
1111
private static volatile int s_osArchPlusOne;
1212

13-
public static string OSDescription => s_osDescription ??= Interop.Sys.GetUnixVersion();
13+
public static string OSDescription => s_osDescription ??= (GetPrettyOSDescription() ?? Interop.Sys.GetUnixVersion());
1414

1515
public static Architecture OSArchitecture
1616
{
@@ -30,5 +30,15 @@ public static Architecture OSArchitecture
3030
return (Architecture)osArch;
3131
}
3232
}
33+
34+
private static string? GetPrettyOSDescription()
35+
{
36+
if (OperatingSystem.IsLinux())
37+
{
38+
return Interop.OSReleaseFile.GetPrettyName();
39+
}
40+
41+
return null;
42+
}
3343
}
3444
}

src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,14 @@ public void VerifyWindowsName()
201201
[Fact, PlatformSpecific(TestPlatforms.Linux)] // Checks Linux name in RuntimeInformation
202202
public void VerifyLinuxName()
203203
{
204-
Assert.Contains("linux", RuntimeInformation.OSDescription, StringComparison.OrdinalIgnoreCase);
204+
if (File.Exists("/etc/os-release"))
205+
{
206+
Assert.Equal(Interop.OSReleaseFile.GetPrettyName("/etc/os-release"), RuntimeInformation.OSDescription);
207+
}
208+
else
209+
{
210+
Assert.Contains("linux", RuntimeInformation.OSDescription, StringComparison.OrdinalIgnoreCase);
211+
}
205212
}
206213

207214
[Fact, PlatformSpecific(TestPlatforms.NetBSD)] // Checks NetBSD name in RuntimeInformation

src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/System.Runtime.InteropServices.RuntimeInformation.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@
1010
<Compile Include="DescriptionNameTests.cs" />
1111
<Compile Include="$(CommonPath)Interop\Linux\cgroups\Interop.cgroups.cs"
1212
Link="Common\Interop\Linux\Interop.cgroups.cs" />
13+
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs"
14+
Link="Interop\Linux\os-release\Interop.OSReleaseFile.cs" />
1315
</ItemGroup>
1416
</Project>

0 commit comments

Comments
 (0)