ldqk / Masuit.Tools

全龄段友好的C#万能工具库,码数吐司库,包含一些常用的操作类,大都是静态类,加密解密,反射操作,权重随机筛选算法,分布式短id,表达式树,linq扩展,文件压缩,多线程下载,硬件信息,字符串扩展方法,日期时间扩展操作,中国农历,大文件拷贝,图像裁剪,验证码,断点续传,集合扩展、Excel导出等常用封装。诸多功能集一身,代码量不到2MB!
https://www.masuit.tools/
MIT License
5.56k stars 1.22k forks source link

[BUG]SaveFileAsync抛出异常ObjectDisposedException #105

Closed li-zheng-hao closed 9 months ago

li-zheng-hao commented 9 months ago

温馨提示:提交后请保证回复及时,若长时间未回复的issue,将在超过30天以后作关闭处理!

bug描述

Stream? convertResult ;
// 省略 ...从http获取stream
if (convertResult != null)
{
    await convertResult.SaveFileAsync(stpFilePath);
    // 下面这种方法没问题
    // await using var fileStream=File.Create(stpFilePath);
    // await convertResult.CopyToAsync(fileStream);
}

是哪个类的哪个函数出现了bug?

异常堆栈信息

System.ObjectDisposedException: Safe handle has been closed.
Object name: 'SafeHandle'.
   at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)
   at Interop.Kernel32.WriteFile(SafeHandle handle, Byte* bytes, Int32 numBytesToWrite, Int32& numBytesWritten, NativeOverlapped* lpOverlapped)
   at System.IO.RandomAccess.WriteAtOffset(SafeFileHandle handle, ReadOnlySpan`1 buffer, Int64 fileOffset)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.ThreadPoolValueTaskSource.ExecuteInternal()
--- End of stack trace from previous location ---
   at Microsoft.Win32.SafeHandles.SafeFileHandle.ThreadPoolValueTaskSource.GetResult(Int16 token)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.ThreadPoolValueTaskSource.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
   at System.Threading.Tasks.ValueTask.ValueTaskSourceAsTask.<>c.<.cctor>b__4_0(Object state)
--- End of stack trace from previous location ---
   at System.IO.BufferedStream.CopyToAsyncCore(Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
   at PDM.Api.MessageConsumers.RecognizeConsumer.ConvertType(FileInfo modelFileInfo, DrawingRecord drawingRecord, String modelFilePath) in E:\Code\PDM_Backend\PDM\PDM.Api\MessageConsumers\RecognizeConsumer.cs:line 
   at PDM.Api.MessageConsumers.RecognizeConsumer.ConsumeAsync(String message, CancellationToken cancellationToken) in E:\Code\PDM_Backend\PDM\PDM.Api\MessageConsumers\RecognizeConsumer.cs:line 

函数入参

期望的结果或输出

正常保存

使用环境:

您的一些其他的想法

针对SaveFileAsync写了一些测试用例,发现复现不了这个问题,但是我换成下面这种写法又不会报错了

await using var fileStream=File.Create(stpFilePath);
await convertResult.CopyToAsync(fileStream);

看了一下源码:

/// <summary>将内存流转储成文件</summary>
/// <param name="ms"></param>
/// <param name="filename"></param>
public static Task SaveFileAsync(this Stream ms, string filename)
{
  using (FileStream destination = new FileStream(filename, FileMode.Create, FileAccess.Write))
    return new BufferedStream(ms, 1048576).CopyToAsync((Stream) destination);
}

感觉是这里哪个Stream在没真正copy时就被销毁了

li-zheng-hao commented 9 months ago
public static async Task SaveToFileAsync(this Stream stream,string filename)
{
    await using FileStream destination = new FileStream(filename, FileMode.Create, FileAccess.Write);
    await new BufferedStream(stream, 1048576).CopyToAsync((Stream) destination);
}

测试了一下换成这种写法也不会报错

ldqk commented 9 months ago

能否提供一个完整的可独立运行可复现这个bug的demo,我测试是可以正常调用不报错的,这是我的demo代码: ConsoleApp1.zip

li-zheng-hao commented 9 months ago

我写了几个测试用例也没能复现,我再尝试一下看看

li-zheng-hao commented 9 months ago

复现了,下面是测试用例:

using Flurl.Http;
using Masuit.Tools.Files;
using Xunit.Abstractions;
[Fact]
public async Task 测试BUG()
{
    for (int i = 0; i < 5; i++)
    {
        var stream=await "https://api.tianditu.gov.cn/cdn/plugins/cesium/Cesium_ext_min.js"
            .GetStreamAsync();
        await stream.SaveFileAsync("test-err" + Guid.NewGuid().ToString());
    }
}

image

用到的库:

  1. Masuit.Tools.Abstractions 2.6.7.4
  2. Flurl.Http 3.2.4
li-zheng-hao commented 9 months ago

修改版本测试用例通过:

[Fact]
public async Task 修复后()
{
    for (int i = 0; i < 5; i++)
    {
        var stream=await "https://api.tianditu.gov.cn/cdn/plugins/cesium/Cesium_ext_min.js"
            .GetStreamAsync();
        // await stream.SaveFileAsync("test-err" + Guid.NewGuid().ToString());
        await FixedSaveToFileAsync(stream, "test-err" + Guid.NewGuid().ToString());
    }
}

static async Task FixedSaveToFileAsync(Stream stream,string filename)
{
    await using FileStream destination = new FileStream(filename, FileMode.Create, FileAccess.Write);
    await new BufferedStream(stream, 1048576).CopyToAsync((Stream) destination);
}
ldqk commented 9 months ago

这个是用法问题了,Http响应流不能直接拿来存文件流,需要先转MemoryStream再用

li-zheng-hao commented 9 months ago

这个是用法问题了,Http响应流不能直接拿来存文件流,需要先转MemoryStream再用

如果是这样的话,扩展方法的this应该用MemoryStream类型更好吧,不然stream的变量点一下之后就出来了这个扩展方法,很容易写出bug诶

ldqk commented 9 months ago

Stream体系本来就很混乱,各种各样的实现,但是又不能限定只能给MemoryStream做扩展,很多其他的流类型也是可以正常转储文件的

ldqk commented 9 months ago

遇到不能直接转储文件的Stream就借助MemoryStream中转一下咯

li-zheng-hao commented 9 months ago

遇到不能直接转储文件的Stream就借助MemoryStream中转一下咯

那也只能这样了 ...🤣