mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-09-29 15:31:04 -04:00
Fix sync disposal of async-created IAsyncDisposable objects (#14755)
This commit is contained in:
parent
2ee887a502
commit
2618a5fba2
@ -19,4 +19,9 @@
|
|||||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
|
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Custom Analyzers -->
|
||||||
|
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' ">
|
||||||
|
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -29,6 +29,9 @@
|
|||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" 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.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.Data.Sqlite" Version="9.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
|
||||||
@ -70,7 +73,7 @@
|
|||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||||
@ -93,4 +96,4 @@
|
|||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@ -55,11 +55,14 @@ public class KeyframeRepository : IKeyframeRepository
|
|||||||
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
|
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var context = _dbProvider.CreateDbContext();
|
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 context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
await using (transaction.ConfigureAwait(false))
|
||||||
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
|
{
|
||||||
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await transaction.CommitAsync(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 />
|
/// <inheritdoc />
|
||||||
|
@ -68,86 +68,88 @@ public class MediaSegmentManager : IMediaSegmentManager
|
|||||||
return;
|
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)
|
|
||||||
{
|
{
|
||||||
// delete all existing media segments if forceOverwrite is set.
|
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
|
||||||
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var provider in providers)
|
|
||||||
{
|
|
||||||
if (!await provider.Supports(baseItem).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
IQueryable<MediaSegment> existingSegments;
|
|
||||||
if (forceOverwrite)
|
if (forceOverwrite)
|
||||||
{
|
{
|
||||||
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
|
// delete all existing media segments if forceOverwrite is set.
|
||||||
}
|
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
else
|
|
||||||
{
|
|
||||||
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestItem = new MediaSegmentGenerationRequest()
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
ItemId = baseItem.Id,
|
if (!await provider.Supports(baseItem).ConfigureAwait(false))
|
||||||
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!forceOverwrite)
|
|
||||||
{
|
{
|
||||||
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
|
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
|
||||||
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IQueryable<MediaSegment> existingSegments;
|
||||||
|
if (forceOverwrite)
|
||||||
|
{
|
||||||
|
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestItem = new MediaSegmentGenerationRequest()
|
||||||
|
{
|
||||||
|
ItemId = baseItem.Id,
|
||||||
|
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!forceOverwrite)
|
||||||
{
|
{
|
||||||
return
|
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
|
||||||
e.StartTicks == f.StartTicks &&
|
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
|
||||||
e.EndTicks == f.EndTicks &&
|
{
|
||||||
e.Type == f.Type;
|
return
|
||||||
})))
|
e.StartTicks == f.StartTicks &&
|
||||||
|
e.EndTicks == f.EndTicks &&
|
||||||
|
e.Type == f.Type;
|
||||||
|
})))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete existing media segments that were re-generated.
|
||||||
|
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
|
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete existing media segments that were re-generated.
|
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
|
||||||
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
var providerId = GetProviderId(provider.Name);
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
segment.ItemId = baseItem.Id;
|
||||||
|
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
|
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
|
|
||||||
var providerId = GetProviderId(provider.Name);
|
|
||||||
foreach (var segment in segments)
|
|
||||||
{
|
|
||||||
segment.ItemId = baseItem.Id;
|
|
||||||
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,24 +159,34 @@ public class MediaSegmentManager : IMediaSegmentManager
|
|||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
|
ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks);
|
||||||
|
|
||||||
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
|
await using (db.ConfigureAwait(false))
|
||||||
await db.SaveChangesAsync().ConfigureAwait(false);
|
{
|
||||||
|
db.MediaSegments.Add(Map(mediaSegment, segmentProviderId));
|
||||||
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
return mediaSegment;
|
return mediaSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteSegmentAsync(Guid segmentId)
|
public async Task DeleteSegmentAsync(Guid segmentId)
|
||||||
{
|
{
|
||||||
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
|
await using (db.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
|
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 db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
await using (db.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -186,36 +198,38 @@ public class MediaSegmentManager : IMediaSegmentManager
|
|||||||
return [];
|
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));
|
|
||||||
|
|
||||||
if (typeFilter is not null)
|
|
||||||
{
|
{
|
||||||
query = query.Where(e => typeFilter.Contains(e.Type));
|
var query = db.MediaSegments
|
||||||
}
|
.Where(e => e.ItemId.Equals(item.Id));
|
||||||
|
|
||||||
if (filterByProvider)
|
if (typeFilter is not null)
|
||||||
{
|
|
||||||
var providerIds = _segmentProviders
|
|
||||||
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
|
|
||||||
.Select(f => GetProviderId(f.Name))
|
|
||||||
.ToArray();
|
|
||||||
if (providerIds.Length == 0)
|
|
||||||
{
|
{
|
||||||
return [];
|
query = query.Where(e => typeFilter.Contains(e.Type));
|
||||||
}
|
}
|
||||||
|
|
||||||
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
|
if (filterByProvider)
|
||||||
}
|
{
|
||||||
|
var providerIds = _segmentProviders
|
||||||
|
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
|
||||||
|
.Select(f => GetProviderId(f.Name))
|
||||||
|
.ToArray();
|
||||||
|
if (providerIds.Length == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return query
|
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
|
||||||
.OrderBy(e => e.StartTicks)
|
}
|
||||||
.AsNoTracking()
|
|
||||||
.AsEnumerable()
|
return query
|
||||||
.Select(Map)
|
.OrderBy(e => e.StartTicks)
|
||||||
.ToArray();
|
.AsNoTracking()
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(Map)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaSegmentDto Map(MediaSegment segment)
|
private static MediaSegmentDto Map(MediaSegment segment)
|
||||||
|
@ -41,14 +41,17 @@ public class FixDates : IAsyncMigrationRoutine
|
|||||||
{
|
{
|
||||||
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
|
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
|
||||||
{
|
{
|
||||||
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var sw = Stopwatch.StartNew();
|
await using (context.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||||
sw.Reset();
|
sw.Reset();
|
||||||
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||||
sw.Reset();
|
sw.Reset();
|
||||||
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
|
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers
|
|||||||
EndProject
|
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}"
|
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
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.CodeAnalysis", "src\Jellyfin.CodeAnalysis\Jellyfin.CodeAnalysis.csproj", "{11643D0F-6761-4EF7-AB71-6F9F8DE00714}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -289,6 +295,7 @@ Global
|
|||||||
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
{A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59} = {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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
|
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
|
||||||
|
@ -169,7 +169,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
{
|
{
|
||||||
if (fileInfo.IsExternal)
|
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 result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||||
var detected = result.Detected;
|
var detected = result.Detected;
|
||||||
@ -937,7 +938,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
.ConfigureAwait(false);
|
.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 result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||||
var charset = result.Detected?.EncodingName ?? string.Empty;
|
var charset = result.Detected?.EncodingName ?? string.Empty;
|
||||||
|
9
src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md
Normal file
9
src/Jellyfin.CodeAnalysis/AnalyzerReleases.Shipped.md
Normal 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'
|
82
src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs
Normal file
82
src/Jellyfin.CodeAnalysis/AsyncDisposalPatternAnalyzer.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
17
src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj
Normal file
17
src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user