Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSDescription.Linux: return a user-friendly name based on /etc/os-release. #83976

Merged
merged 6 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
tmds marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Returns a user-friendly distribution name.
/// </summary>
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<char> prettyName = default, name = default, version = default;
foreach (string line in lines)
{
ReadOnlySpan<char> 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<char> line, ReadOnlySpan<char> prefix, ref ReadOnlySpan<char> value)
{
if (!line.StartsWith(prefix))
{
return false;
}
ReadOnlySpan<char> 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;
Copy link
Member

@stephentoub stephentoub Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd find this easier to read if this were moved up to above the static local function.

}
}
}
3 changes: 3 additions & 0 deletions src/libraries/Common/tests/Common.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
Link="Common\Interop\Linux\procfs\Interop.ProcFsStat.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.TryReadStatusFile.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.TryReadStatusFile.cs" />
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs"
Link="Common\Interop\Linux\os-release\Interop.OSReleaseFile.cs" />
<Compile Include="$(CommonPath)System\CharArrayHelpers.cs"
Link="Common\System\CharArrayHelpers.cs" />
<Compile Include="$(CommonPath)System\StringExtensions.cs"
Expand Down Expand Up @@ -78,6 +80,7 @@
Link="System\PasteArguments.cs" />
<Compile Include="Tests\Interop\cgroupsTests.cs" />
<Compile Include="Tests\Interop\procfsTests.cs" />
<Compile Include="Tests\Interop\OSReleaseTests.cs" />
<Compile Include="Tests\System\IO\PathInternal.Tests.cs" />
<Compile Include="Tests\System\IO\StringParserTests.cs" />
<Compile Include="Tests\System\Net\HttpDateParserTests.cs" />
Expand Down
61 changes: 61 additions & 0 deletions src/libraries/Common/tests/Tests/Interop/OSReleaseTests.cs
Original file line number Diff line number Diff line change
@@ -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<Exception>(() => File.ReadAllText(path));

string? name = Interop.OSReleaseFile.GetPrettyName(path);
Assert.Null(name);
}
}
}
38 changes: 17 additions & 21 deletions src/libraries/Common/tests/Tests/Interop/procfsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2379,6 +2379,7 @@
<Compile Include="$(CommonPath)System\IO\StringParser.cs" Condition="'$(TargetsLinux)' == 'true'" Link="Common\System\IO\StringParser.cs" />
<Compile Include="$(CommonPath)Interop\OSX\Interop.libproc.GetProcessInfoById.cs" Condition="'$(TargetsOSX)' == 'true' or '$(TargetsMacCatalyst)' == 'true'" Link="Common\Interop\OSX\Interop.libproc.GetProcessInfoById.cs" />
<Compile Include="$(CommonPath)Interop\SunOS\procfs\Interop.ProcFsStat.TryReadProcessStatusInfo.cs" Condition="'$(Targetsillumos)' == 'true' or '$(TargetsSolaris)' == 'true'" Link="Common\Interop\SunOS\Interop.ProcFsStat.TryReadProcessStatusInfo.cs" />
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs" Condition="'$(TargetsBrowser)' != 'true' and '$(TargetsWasi)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.FreeBSD.cs" Condition="'$(TargetsFreeBSD)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Linux.cs" Condition="'$(TargetsLinux)' == 'true'" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -30,5 +30,15 @@ public static Architecture OSArchitecture
return (Architecture)osArch;
}
}

private static string? GetPrettyOSDescription()
{
if (OperatingSystem.IsLinux())
{
return Interop.OSReleaseFile.GetPrettyName();
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
tmds marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
Assert.Contains("linux", RuntimeInformation.OSDescription, StringComparison.OrdinalIgnoreCase);
}
}

[Fact, PlatformSpecific(TestPlatforms.NetBSD)] // Checks NetBSD name in RuntimeInformation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
<Compile Include="DescriptionNameTests.cs" />
<Compile Include="$(CommonPath)Interop\Linux\cgroups\Interop.cgroups.cs"
Link="Common\Interop\Linux\Interop.cgroups.cs" />
<Compile Include="$(CommonPath)Interop\Linux\os-release\Interop.OSReleaseFile.cs"
Link="Interop\Linux\os-release\Interop.OSReleaseFile.cs" />
</ItemGroup>
</Project>