記 QEMU 虛擬磁碟裝置移動檔案拋異常但實際移動成功

lindexi發表於2024-11-15

我使用 Get-WmiObject Win32_DiskDrive 命令在 PowerShell 裡面呼叫 WMI 讀取硬碟資訊,可以看到如下輸出內容

Get-WmiObject Win32_DiskDrive
Partitions : 1
DeviceID   : \\.\PHYSICALDRIVE1
Model      : QEMU QEMU HARDDISK SCSI Disk Device
Size       : 105226007040
Caption    : QEMU QEMU HARDDISK SCSI Disk DevicePartitions : 2
DeviceID   : \\.\PHYSICALDRIVE0
Model      : QEMU QEMU HARDDISK SCSI Disk Device
Size       : 118106795520
Caption    : QEMU QEMU HARDDISK SCSI Disk DevicePartitions : 1
DeviceID   : \\.\PHYSICALDRIVE2
Model      : QEMU QEMU HARDDISK SCSI Disk Device
Size       : 118106795520
Caption    : QEMU QEMU HARDDISK SCSI Disk Device

透過裝置管理器能看到如下介面

經過 lsj 的分析,預計是虛擬磁碟的問題,導致移動成功了,但是 Win32 給出錯誤資訊

為了更好復現問題,我編寫了以下測試程式。經過我的測試,這個問題和 WPF 無關,且和 dotnet 無關,和網路下載無關。僅僅只是以下簡單的多個檔案同時寫入,寫入之後進行移動,就能復現問題

var workFolder = Path.Join(Path.GetTempPath(), "Test-RecedajeeKewhinuhoyay");
Directory.CreateDirectory(workFolder);

Console.WriteLine($"測試資料夾 {workFolder}");

var locker = new object();

var taskList = new List<Task>();

for (int i = 0; i < 50; i++)
{
    var task = Task.Run(async () =>
    {
        var fileName = Path.GetRandomFileName();
        var filePath = Path.Join(workFolder, fileName + ".tmp");

        await MockDownloadAsync(filePath);

        var newFilePath = Path.Join(workFolder, fileName);
        try
        {
            File.Move(filePath, newFilePath);
        }
        catch (Exception e)
        {
            /*
            Move file fail. FilePath=C:\Users\admin\AppData\Local\Temp\Test-RecedajeeKewhinuhoyay\zvxau5gx.lmz.tmp. HResult=80070003;System.IO.DirectoryNotFoundException: Could not find a part of the path.
               at System.IO.FileSystem.MoveFile(String, String, Boolean)
               at System.IO.File.Move(String, String, Boolean)
               at System.IO.File.Move(String, String)
               at Program.<>c__DisplayClass0_0.<<<Main>$>b__2>d.MoveNext()
             */

            // 新檔案存在: True;原檔案存在: False

            if (e is IOException ioException)
            {
                Output($"Move file fail. FilePath={filePath}. HResult={ioException.HResult:X};\r\n新檔案存在: {File.Exists(newFilePath)};原檔案存在: {File.Exists(filePath)}\r\n{ioException}");
            }
            else
            {
                Output($"Move file fail. FilePath={filePath}.\r\n新檔案存在: {File.Exists(newFilePath)};原檔案存在: {File.Exists(filePath)}\r\n{e}");
            }
        }
    });

    taskList.Add(task);
}

void Output(string message)
{
    Console.WriteLine(message);
}

async Task MockDownloadAsync(string filePath)
{
    using var fileStream = File.OpenWrite(filePath);
    var buffer = new byte[10240];
    for (int i = 0; i < 100; i++)
    {
        Random.Shared.NextBytes(buffer);
        fileStream.Write(buffer, 0, buffer.Length);

        await Task.Delay(15);
    }
}

Task.WaitAll(taskList);

Console.WriteLine("執行完成");

以上程式碼使用 MockDownloadAsync 方法代替真實的網路下載。同時執行多個檔案的寫入,似乎能夠讓虛擬磁碟更加忙碌,於是就能夠復現問題

丟擲的異常如下

System.IO.DirectoryNotFoundException: Could not find a part of the path.

               at System.IO.FileSystem.MoveFile(String, String, Boolean)
               at System.IO.File.Move(String, String, Boolean)
               at System.IO.File.Move(String, String)
               at Program.<>c__DisplayClass0_0.<<<Main>$>b__2>d.MoveNext()

對應的 Win32 錯誤碼是 0x80070003

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin a3b5ce1d8dcc08cdc2d23e436e6fb477f1fac503

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin a3b5ce1d8dcc08cdc2d23e436e6fb477f1fac503

獲取程式碼之後,進入 Workbench/RecedajeeKewhinuhoyay 資料夾,即可獲取到原始碼

更多技術部落格,請參閱 部落格導航

相關文章