System.IO.FileSystemWatcher的坑

System.IO命名空間下面有一個FileSystemWatcher,這個東西可以實現文件變動的提醒。需要監控文件夾變化(比如FTP伺服器)的情形非常適用。

需要監控文件新建時,我們可以這麼寫:

_fileSystemWatcher.Path = path;
_fileSystemWatcher.IncludeSubdirectories = true;
_fileSystemWatcher.Created += _fileSystemWatcher_Created;
_fileSystemWatcher.EnableRaisingEvents = true;

protected async void _fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
{
Console.WriteLine(e.FullPath);
}

感覺還是挺方便的吧?接下來就是坑了。

傳輸延遲問題

FileSystemWatcher只要發現文件創建就觸發了,大文件或者FTP等需要一段時間才能完成傳輸的情況下,直接在時間處理程式中處理文件會由於文件不完整導致錯誤。可惜的是,FileSystemWatcher並沒有內建任何機制可以保障文件傳輸完成再觸發Created事件,我們只能靠自己程式碼保障。

以下程式碼運行於.NET 6,Windows 11,Rocky Linux 9

Windows only方案

  • FileSystemWatcher除了Created,還提供了Changed事件,我們可以先監聽Created事件,然後再監控Changed的情況,當文件屬性不在變化時,認為是傳輸完畢了。
    這種方案可行,不過感覺有點太麻煩了,我需要監聽兩個事件,還需要處理先後順序,其實我只想知道創建而已…

  • 在Created事件中,使用排他性的文件打開操作
    在File.Open()函數中,有重載可以提供獨佔的訪問,訪問不成功,文件會彈出錯誤。

            //防止文件上傳時間過長,導致無法正常識別
            if (!File.Exists(e.FullPath)) return;
            var accessable = false;
            for (int i = 0; i < 5; i++)
            {
                try
                {
                    using (File.Open(e.FullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                    {
                        Console.WriteLine("Break");
                        accessable = true;
                        break;
                    }
                }
                catch (Exception)
                {
                    Console.WriteLine("Loop" + i);
                }
                await Task.Delay(3000);
            }
            //文件超時無法讀取,失敗。
            if (!accessable) return;
//後續程式碼

運行可以看見這樣的輸出,說明方案可行。

Linux與Windows通用方案

上面的方案似乎已經解決了我們的問題,我興緻勃勃地部署到Linux機器上時卻死活無法正常工作,Debug發現Open()這個方法居然可以一次直接通過,看來Linux下的Share不能正常獨佔這個文件,還得換一個方法。

protected async void _fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
        {
            //防止文件上傳時間過長,導致無法正常識別
            if (!File.Exists(e.FullPath)) return;
            var accessable = false;
            for (int i = 0; i < 5; i++)
            {
                await Task.Delay(3000);
                Console.WriteLine("loop" + i);
                var time1 = File.GetLastWriteTimeUtc(e.FullPath);
                await Task.Delay(1000);
                var time2 = File.GetLastWriteTimeUtc(e.FullPath);
                if (time1 == time2)
                {
                    accessable = true;
                    break;
                }
            }
            //文件超時無法讀取,失敗。
            if (!accessable) return;
//後續程式碼
}

我們可以在程式中定時檢查文件的最後修改時間,如果相隔一段時間的兩次最後修改時間一致的話,那說明文件已經完成了傳輸,這種方式不依賴於打開操作,並且可以在Windows和Linux下運行。

為了防止無限循環,設置了超時,如果在指定的時間內無法完成,那麼程式直接跳出。

參考