Fix sync disposal of async-created IAsyncDisposable objects (#14755)

This commit is contained in:
evan314159 2025-09-16 17:14:52 +08:00 committed by GitHub
parent 2ee887a502
commit 2618a5fba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 256 additions and 111 deletions

View File

@ -19,4 +19,9 @@
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
</ItemGroup>
<!-- Custom Analyzers -->
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' ">
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@ -29,6 +29,9 @@
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />

View File

@ -55,12 +55,15 @@ public class KeyframeRepository : IKeyframeRepository
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
{
using var context = _dbProvider.CreateDbContext();
using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)

View File

@ -68,8 +68,9 @@ public class MediaSegmentManager : IMediaSegmentManager
return;
}
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
if (forceOverwrite)
@ -151,31 +152,42 @@ public class MediaSegmentManager : IMediaSegmentManager
}
}
}
}
/// <inheritdoc />
public async Task<MediaSegmentDto> CreateSegmentAsync(MediaSegmentDto mediaSegment, string segmentProviderId)
{
ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
await db.SaveChangesAsync().ConfigureAwait(false);
}
return mediaSegment;
}
/// <inheritdoc />
public async Task DeleteSegmentAsync(Guid segmentId)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem? item, IEnumerable<MediaSegmentType>? typeFilter, LibraryOptions libraryOptions, bool filterByProvider = true)
@ -186,8 +198,9 @@ public class MediaSegmentManager : IMediaSegmentManager
return [];
}
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
var query = db.MediaSegments
.Where(e => e.ItemId.Equals(item.Id));
@ -217,6 +230,7 @@ public class MediaSegmentManager : IMediaSegmentManager
.Select(Map)
.ToArray();
}
}
private static MediaSegmentDto Map(MediaSegment segment)
{

View File

@ -41,7 +41,9 @@ public class FixDates : IAsyncMigrationRoutine
{
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
{
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var sw = Stopwatch.StartNew();
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
@ -51,6 +53,7 @@ public class FixDates : IAsyncMigrationRoutine
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
}
}
}
private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
{

View File

@ -96,6 +96,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.CodeAnalysis", "src\Jellyfin.CodeAnalysis\Jellyfin.CodeAnalysis.csproj", "{11643D0F-6761-4EF7-AB71-6F9F8DE00714}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -258,6 +260,10 @@ Global
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.Build.0 = Release|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11643D0F-6761-4EF7-AB71-6F9F8DE00714}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -289,6 +295,7 @@ Global
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
{8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
{11643D0F-6761-4EF7-AB71-6F9F8DE00714} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

View File

@ -169,7 +169,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (fileInfo.IsExternal)
{
using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
var detected = result.Detected;
@ -937,7 +938,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
.ConfigureAwait(false);
}
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
var charset = result.Detected?.EncodingName ?? string.Empty;

View File

@ -0,0 +1,9 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
## Release 1.0
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
JF0001 | Usage | Warning | Async-created IAsyncDisposable objects should use 'await using'

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Jellyfin.CodeAnalysis;
/// <summary>
/// Analyzer to detect sync disposal of async-created IAsyncDisposable objects.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncDisposalPatternAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic descriptor for sync disposal of async-created IAsyncDisposable objects.
/// </summary>
public static readonly DiagnosticDescriptor AsyncDisposableSyncDisposal = new(
id: "JF0001",
title: "Async-created IAsyncDisposable objects should use 'await using'",
messageFormat: "Using 'using' with async-created IAsyncDisposable object '{0}'. Use 'await using' instead to prevent resource leaks.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Objects that implement IAsyncDisposable and are created using 'await' should be disposed using 'await using' to prevent resource leaks.");
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AsyncDisposableSyncDisposal];
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeUsingStatement, SyntaxKind.UsingStatement);
}
private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context)
{
var usingStatement = (UsingStatementSyntax)context.Node;
// Skip 'await using' statements
if (usingStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
return;
}
// Check if there's a variable declaration
if (usingStatement.Declaration?.Variables is null)
{
return;
}
foreach (var variable in usingStatement.Declaration.Variables)
{
if (variable.Initializer?.Value is AwaitExpressionSyntax awaitExpression)
{
var typeInfo = context.SemanticModel.GetTypeInfo(awaitExpression);
var type = typeInfo.Type;
if (type is not null && ImplementsIAsyncDisposable(type))
{
var diagnostic = Diagnostic.Create(
AsyncDisposableSyncDisposal,
usingStatement.GetLocation(),
type.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
private static bool ImplementsIAsyncDisposable(ITypeSymbol type)
{
return type.AllInterfaces.Any(i =>
string.Equals(i.Name, "IAsyncDisposable", StringComparison.Ordinal)
&& string.Equals(i.ContainingNamespace?.ToDisplayString(), "System", StringComparison.Ordinal));
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
</Project>