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

Support .NET 9.0 #30

Merged
merged 5 commits into from
Oct 22, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
matrix:
runs-on: ['ubuntu-latest']
configuration: [Debug, Release]
dotnet-version: ['8.0.x']
tfm: ['net8.0']
dotnet-version: ['9.0.x']
tfm: ['net8.0', 'net9.0']
uses: ./.github/workflows/dotnet-reusable-workflow.yml
with:
runs-on: ${{ matrix.runs-on }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
matrix:
runs-on: ['windows-latest']
configuration: [Debug, Release]
dotnet-version: ['8.0.x']
tfm: ['net8.0', 'net481']
dotnet-version: ['9.0.x']
tfm: ['net8.0', 'net9.0', 'net481']
uses: ./.github/workflows/dotnet-reusable-workflow.yml
with:
runs-on: ${{ matrix.runs-on }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/upload-coverage-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ jobs:
env:
OUTPUT_DIR: _site/coverage
WORKFLOW_SPEC: >
dotnet-windows.yml: coverage-report-windows-net8.0-Release,coverage-report-windows-net481-Release |
dotnet-ubuntu.yml: coverage-report-ubuntu-net8.0-Release
dotnet-windows.yml: coverage-report-windows-net8.0-Release,coverage-report-windows-net9.0-Release,coverage-report-windows-net481-Release |
dotnet-ubuntu.yml: coverage-report-ubuntu-net8.0-Release,coverage-report-ubuntu-net9.0-Release
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
10 changes: 5 additions & 5 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<UseArtifactsOutput>true</UseArtifactsOutput>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>

<TargetFrameworks>net8.0;net481</TargetFrameworks>

<Nullable Condition="'$(TargetFramework)' == 'net8.0'">enable</Nullable>
<Nullable Condition="'$(TargetFramework)' == 'net481'">annotations</Nullable>
<TargetFrameworks>net8.0;net9.0;net481</TargetFrameworks>

<IsNetFramework>false</IsNetFramework>
<IsNetFramework Condition="'$(TargetFramework)' == 'net481'">true</IsNetFramework>


<Nullable Condition="'$(IsNetFramework)' != 'true'">enable</Nullable>
<Nullable Condition="'$(IsNetFramework)' == 'true'">annotations</Nullable>

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);CA1031;CA1303;CA1416;CA1801;CA1716;NU5105</NoWarn>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ Example:

| Build [^1] | Coverage [^2] |
| -----------| --------------|
| [![](https://github.com/cklutz/LockCheck/workflows/Windows/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AWindows) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net481-release%2FSummary.json&query=%24.summary.linecoverage&label=net%204.8&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net481-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Ubuntu/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AUbuntu) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net8.0-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Windows/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AWindows) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net9.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%209.0&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net9.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fwindows-net481-release%2FSummary.json&query=%24.summary.linecoverage&label=net%204.8&suffix=%25)](https://cklutz.github.io/LockCheck/windows-net481-release) |
| [![](https://github.com/cklutz/LockCheck/workflows/Ubuntu/badge.svg)](https://github.com/cklutz/LockCheck/actions?query=workflow%3AUbuntu) | [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net8.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%208.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net8.0-release) [![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcklutz.github.io%2FLockCheck%2Fubuntu-net9.0-release%2FSummary.json&query=%24.summary.linecoverage&label=net%209.0&suffix=%25)](https://cklutz.github.io/LockCheck/ubuntu-net9.0-release) |





[^1]: A build is done for every supported target framework for that platform (currently for Windows this is .NET 8.0 and .NET Framework 4.8, for Linux/Ubuntu this is .NET 8.0) in every supported configuration (Release and Debug).
[^2]: Code coverage is generated separately for every supported target framework for a platform, but only for the Release configuration. It is updated nightly from the latest build of the main branch.
Expand Down
63 changes: 61 additions & 2 deletions src/LockCheck/Linux/InodeInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;

namespace LockCheck.Linux
Expand All @@ -7,9 +7,67 @@ internal readonly struct InodeInfo
{
public static bool TryParse(ReadOnlySpan<char> field, out InodeInfo value)
{
#if NET9_0_OR_GREATER
// This implementation *look* more complex and thus slower, but is actually faster:
//
// | Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Allocated | Alloc Ratio |
// |------- |---------:|---------:|---------:|------:|--------:|----------:|----------:|------------:|
// | Old | 71.97 ns | 1.462 ns | 1.900 ns | 1.00 | 0.04 | 1,604 B | - | NA |
// | NET9.0 | 51.40 ns | 0.800 ns | 0.668 ns | 0.71 | 0.02 | 1,355 B | - | NA |

int num = 0;
int major = 0;
int minor = 0;
long number = 0;

foreach (var range in field.Split(':'))
{
var fieldContent = field[range].Trim();

switch (num)
{
case 0:
if (!int.TryParse(fieldContent, NumberStyles.HexNumber, null, out major))
{
goto fail;
}
break;
case 1:
if (!int.TryParse(fieldContent, NumberStyles.HexNumber, null, out minor))
{
goto fail;
}
break;
case 2:
if (!long.TryParse(fieldContent, NumberStyles.Integer, null, out number))
{
goto fail;
}
break;
}

if (++num > 2)
{
// Ignore additional fields
break;
}
}

if (num < 2)
{
// Not enough fields
goto fail;
}

value = new InodeInfo(major, minor, number);
return true;
fail:
value = default;
return false;
#else
int count = field.Count(':') + 1;
Span<Range> ranges = count < 128 ? stackalloc Range[count] : new Range[count];
int num = MemoryExtensions.Split(field, ranges, ':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
int num = MemoryExtensions.Split(field, ranges, ':', StringSplitOptions.TrimEntries);
if (num < 3 ||
!int.TryParse(field[ranges[0]], NumberStyles.HexNumber, null, out int major) ||
!int.TryParse(field[ranges[1]], NumberStyles.HexNumber, null, out int minor) ||
Expand All @@ -21,6 +79,7 @@ public static bool TryParse(ReadOnlySpan<char> field, out InodeInfo value)

value = new InodeInfo(major, minor, number);
return true;
#endif
}

private InodeInfo(int majorDeviceId, int minorDeviceId, long iNodeNumber)
Expand Down
50 changes: 44 additions & 6 deletions src/LockCheck/Linux/ProcFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,16 +385,54 @@ internal static string[] ConvertToArgs(ref Span<byte> buffer, int maxArgs = -1)
return args?.Length > 0 ? args[0] : null;
}

private static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
internal static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
{
int count = content.Count(delimiter) + 1;
Span<Range> ranges = count < 128 ? stackalloc Range[count] : new Range[count];
int num = MemoryExtensions.Split(content, ranges, delimiter);
if (index >= num)
if (index < 0)
{
throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {num} fields available.");
throw new ArgumentOutOfRangeException(nameof(index), index, $"Field index cannot be negative.");
}

#if NET9_0_OR_GREATER
// PERF NOTE: For larger index values this will perform actually worse than the manual usage of
// Count()/MemoryExtensions.Split() below. However, currently we use rather small indexes (5 out of 52)
// where this performs actually better.
// Also, this is cleaner an less error prone.
// In .NET 10+ the ref struct enumerator returned here will implement IEnumerable<> so that we could
// try using LINQ here, to make things even more simple (and possibly performant, as LINQ is getting
// improved also!)

int count = 0;
foreach (var range in content.Split(delimiter))
{
if (count < index)
{
count++;
continue;
}
return content[range];
}

throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {count} fields available.");
#else
int fieldCount = content.Count(delimiter) + 1;
if (fieldCount <= index)
{
throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {fieldCount} fields available.");
}

// We need to split into N+1 fields, where N is the field denoted by the index.
// The extra field will receive the remainder of content, that doesn't need to
// be split further, because we're not interested. That also means, that if we
// are supposed to read the last field of content, we don't need that extra field.
int rangeCount = index == fieldCount - 1 ? index + 1 : index + 2;
Span<Range> ranges = rangeCount < 128 ? stackalloc Range[rangeCount] : new Range[rangeCount];
int num = MemoryExtensions.Split(content, ranges, delimiter);

// Shouldn't trigger, because of pre-checks done above.
Debug.Assert(num == rangeCount);

return content[ranges[index]];
#endif
}
}
}
25 changes: 20 additions & 5 deletions src/LockCheckTool/LockCheckTool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable Condition="'$(TargetFramework)' != 'net8.0'">false</IsPackable>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

<PropertyGroup>
<RollForward>major</RollForward>
<Version>1.0.1</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>lockcheck</ToolCommandName>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>lockchecktool</PackageId>
<Authors>cklutz</Authors>
<Description>A tool to list processes locking a given file.</Description>
Expand All @@ -24,4 +22,21 @@
<ProjectReference Include="..\LockCheck\LockCheck.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.143" PrivateAssets="all" />
</ItemGroup>

<Target Name="AdjustPackagingDirectives" AfterTargets="CoreCompile">
<!-- Prevent "error NETSDK1054: only supports .NET Core."
Since we still multi target net481 as well, we'd get this error otherwise.
We cannot set these directly, because they are need to be set for cross-targeting
and not outer builds.
-->
<PropertyGroup>
<PackAsTool Condition="'$(TargetFramework)' == 'net481'">false</PackAsTool>
<IsPackable Condition="'$(TargetFramework)' == 'net481'">false</IsPackable>
<GeneratePackageOnBuild Condition="'$(TargetFramework)' == 'net481'">false</GeneratePackageOnBuild>
</PropertyGroup>
</Target>

</Project>
22 changes: 22 additions & 0 deletions test/LockCheck.Tests/Linux/ProcFileSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,27 @@ public void ConvertToArgs_ShouldReturnMaxArgs_WhenMaxArgsIsSet(int maxArgs, stri
Assert.AreEqual(expected.Length, args.Length);
CollectionAssert.AreEqual(expected, args);
}

[DataTestMethod]
[DataRow(0, "223424")]
[DataRow(1, "(bash)")]
[DataRow(2, "S")]
[DataRow(3, "223420")]
public void GetField_ShouldReturnField_WhenIndexIsInBounds(int index, string expected)
{
var result = ProcFileSystem.GetField("223424 (bash) S 223420".AsSpan(), ' ', index);
Assert.AreEqual(expected, result.ToString());
}

[DataTestMethod]
[DataRow(-1)]
[DataRow(4)]
[DataRow(int.MinValue)]
[DataRow(int.MaxValue)]
public void GetField_ShouldThrowArgumentOutOfRangeException_WhenIndexIsOutOfBounds(int index)
{
var ex = Assert.ThrowsException<ArgumentOutOfRangeException>(() => ProcFileSystem.GetField("223424 (bash) S 223420".AsSpan(), ' ', index));
Console.WriteLine(ex);
}
}
}
37 changes: 36 additions & 1 deletion test/test-docker-linux.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
docker run --rm --name LockCheck.Tests -v ${PSScriptRoot}/..:/mnt/lc -w /mnt/lc mcr.microsoft.com/dotnet/sdk:8.0 bash /mnt/lc/test/test-linux.sh
$ErrorActionPreference = 'Stop'

$SourcesRootDir=((Get-Item $PSScriptRoot).Parent.FullName)
$ContainerWorkDir="/mnt/lc"
$ImageName="$env:USERNAME-lockcheck-tests"
$ContainerName="LockCheck.Tests"
$ContextDir="$SourcesRootDir\artifacts\TestContainer"
$DockerFileContent=@'
FROM mcr.microsoft.com/dotnet/sdk:9.0 as build
# Copy .NET 8.0 runtime files
COPY --from=mcr.microsoft.com/dotnet/sdk:8.0 /usr/share/dotnet/shared /usr/share/dotnet/shared
'@

# Create a docker image that combines multiple dotnet versions
mkdir -Force $ContextDir | Out-Null
echo $DockerFileContent > "$ContextDir\Dockerfile"
docker build -q -t $ImageName $ContextDir
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to create container"
exit 1
}

# Run tests
docker run --rm --name $ContainerName -v ${SourcesRootDir}:$ContainerWorkDir -w $ContainerWorkDir $ImageName bash $ContainerWorkDir/test/test-linux.sh
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to run tests"
exit 1
}

# Don't rely on "prune" to be run eventually.
# If tests were successfull we don't need it anymore.
docker image rm $ImageName
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to remove image $ImageName"
exit 1
}
15 changes: 13 additions & 2 deletions test/test-linux.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
#!/bin/bash
#
# Run test cycle on linux. This script can be run directly from within Linux (e.g. WSL),
# and also serves as the driver to be run inside a docker container (see test-docker-linux.ps1).
#

frameworks=('net8.0')
frameworks=('net8.0' 'net9.0')
configurations=('Release' 'Debug')
platforms=('x64')
project="$(dirname $0)/LockCheck.Tests/LockCheck.Tests.csproj"
resultsDir="$(dirname $0)/../artifacts/TestResults"

export DOTNET_CLI_TELEMETRY_OPTOUT=1

# TODO: This issue https://github.com/dotnet/sdk/issues/29742 prevents us from running
# the build separately, like on Windows, and thus less often then with every test
# combination.
for framework in "${frameworks[@]}"; do
for configuration in "${configurations[@]}"; do
for platform in "${platforms[@]}"; do
echo -e "\n\033[34m[$framework - $configuration - $platform]\033[0m"
/usr/share/dotnet/dotnet test --logger console -c $configuration -f $framework -a $platform $project || exit 1
runPivot=$(echo "${configuration}_${framework}_linux-${platform}" | tr '[:upper:]' '[:lower:]')
/usr/share/dotnet/dotnet test --logger console --results-directory "$resultsDir/$runPivot" -c $configuration -f $framework -a $platform $project || exit 1
done
done
done
19 changes: 16 additions & 3 deletions test/test-windows.ps1
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
$frameworks = ('net481', 'net8.0')
$frameworks = ('net481', 'net8.0', 'net9.0')
$configurations = ('Debug', 'Release')
$platforms = ('x86', 'x64')
$project = "$PSScriptRoot\LockCheck.Tests\LockCheck.Tests.csproj"
$resultsDir = "$((Get-Item $PSScriptRoot).Parent.FullName)\artifacts\TestResults"

$env:DOTNET_CLI_TELEMETRY_OPTOUT=1

# Build once for every configuration (platforms and frameworks will be handled automatically)
# foreach ($configuration in $configurations) {
# & dotnet build -c $configuration $project
# if ($LASTEXITCODE -ne 0) {
# exit 1
# }
# }

# Run tests; dedicated per framework/configuration/platform so that the test runner itself can
# also uses the desired platform.
foreach ($framework in $frameworks) {
foreach ($platform in $platforms) {
foreach ($configuration in $configurations) {
Write-Host -Foreground DarkBlue "`n[$framework - $configuration - $platform]"

& dotnet test -c $configuration -f $framework -a $platform $project
$runPivot = "${configuration}_${framework}_win-${platform}".ToLowerInvariant()
& dotnet test --results-directory "$resultsDir\$runPivot" -c $configuration -f $framework -a $platform "$project"
if ($LASTEXITCODE -ne 0) {
exit 1
}
Expand Down
File renamed without changes.