使用 FileSystemWatcher 和 C# 处理包含多个文件的文件夹

Processing Folder With Multiple Files Using FileSystemWatcher and C#

我创建了一个相对简单的 windows 应用程序来监视文件夹中的文件。当在文件夹中创建新文件时,应用程序(通过 FileSystemWatcher)将打开文件并处理内容。长话短说,内容与 Selenium 一起使用,通过 IE11 自动化网页。此处理每个文件大约需要 20 秒。

问题是,如果多个文件大致同时创建到文件夹中,或者当应用程序正在处理一个文件时,FileSystemWatcher onCreated 看不到下一个文件。因此,当第一个文件的处理完成时,应用程序就会停止。同时文件夹中有一个文件没有得到处理。如果在 onCreated 处理完成后添加文件,它可以正常工作并处理下一个文件。

有人可以指导我解决这个问题应该注意什么吗?非常欢迎提供过多的细节。

您可以使用 P/Invoke 到 运行 Win32 文件系统更改通知功能,而不是使用 FileSystemWatcher,并在文件系统更改发生时循环:

[DllImport("kernel32.dll", EntryPoint = "FindFirstChangeNotification")]
static extern System.IntPtr FindFirstChangeNotification (string lpPathName, bool bWatchSubtree, uint dwNotifyFilter);

[DllImport("kernel32.dll", EntryPoint = "FindNextChangeNotification")]
static extern bool FindNextChangeNotification (System.IntPtr hChangedHandle);

[DllImport("kernel32.dll", EntryPoint = "FindCloseChangeNotification")]
static extern bool FindCloseChangeNotification (System.IntPtr hChangedHandle);

[DllImport("kernel32.dll", EntryPoint = "WaitForSingleObject")]
static extern uint WaitForSingleObject (System.IntPtr handle, uint dwMilliseconds);

[DllImport("kernel32.dll", EntryPoint = "ReadDirectoryChangesW")]
static extern bool ReadDirectoryChangesW(System.IntPtr hDirectory, System.IntPtr lpBuffer, uint nBufferLength, bool bWatchSubtree, uint dwNotifyFilter, out uint lpBytesReturned, System.IntPtr lpOverlapped, ReadDirectoryChangesDelegate lpCompletionRoutine);

基本上,您使用要监视的目录调用 FindFirstChangeNotification,这会为您提供等待句柄。然后您使用句柄调用 WaitForSingleObject,当它 returns 时,您知道发生了一个或多个更改。然后,您调用 ReadDirectoryChangesW 找出发生了什么变化,并处理这些变化。调用 FindNextChangeNotification 为您提供等待文件系统下一次更改的句柄,因此您可能会调用它,然后调用 WaitForSingleObject,然后在循环中调用 ReadDirectoryChangesW。完成后,您可以调用 FindCloseChangeNotification 停止跟踪更改。

编辑:这是一个更完整的示例:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

[DllImport("kernel32.dll", EntryPoint = "FindFirstChangeNotification")]
static extern System.IntPtr FindFirstChangeNotification(string lpPathName, bool bWatchSubtree, uint dwNotifyFilter);

[DllImport("kernel32.dll", EntryPoint = "FindNextChangeNotification")]
static extern bool FindNextChangeNotification(System.IntPtr hChangedHandle);

[DllImport("kernel32.dll", EntryPoint = "FindCloseChangeNotification")]
static extern bool FindCloseChangeNotification(System.IntPtr hChangedHandle);

[DllImport("kernel32.dll", EntryPoint = "WaitForSingleObject")]
static extern uint WaitForSingleObject(System.IntPtr handle, uint dwMilliseconds);

[DllImport("kernel32.dll", EntryPoint = "ReadDirectoryChangesW")]
static extern bool ReadDirectoryChangesW(System.IntPtr hDirectory, System.IntPtr lpBuffer, uint nBufferLength, bool bWatchSubtree, uint dwNotifyFilter, out uint lpBytesReturned, System.IntPtr lpOverlapped, IntPtr lpCompletionRoutine);

[DllImport("kernel32.dll", EntryPoint = "CreateFile")]
public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr SecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);

enum FileSystemNotifications
{
    FileNameChanged = 0x00000001,
    DirectoryNameChanged = 0x00000002,
    FileAttributesChanged = 0x00000004,
    FileSizeChanged = 0x00000008,
    FileModified = 0x00000010,
    FileSecurityChanged = 0x00000100,
}

enum FileActions
{
    FileAdded = 0x00000001,
    FileRemoved = 0x00000002,
    FileModified = 0x00000003,
    FileRenamedOld = 0x00000004,
    FileRenamedNew = 0x00000005
}

enum FileEventType
{
    FileAdded,
    FileChanged,
    FileDeleted,
    FileRenamed
}

class FileEvent
{
    private readonly FileEventType eventType;
    private readonly FileInfo file;

    public FileEvent(string fileName, FileEventType eventType)
    {
        this.file = new FileInfo(fileName);
        this.eventType = eventType;
    }

    public FileEventType EventType => eventType;
    public FileInfo File => file;
}

[StructLayout(LayoutKind.Sequential)]
struct FileNotifyInformation
{
    public int NextEntryOffset;
    public int Action;
    public int FileNameLength;
    public IntPtr FileName;
}

class DirectoryWatcher
{
    private const int MaxChanges = 4096;
    private readonly DirectoryInfo directory;

    public DirectoryWatcher(string dirPath)
    {
        this.directory = new DirectoryInfo(dirPath);
    }

    public IEnumerable<FileEvent> Watch(bool watchSubFolders = false)
    {
        var directoryHandle = CreateFile(directory.FullName, 0x80000000, 0x00000007, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero);    
        var fileCreatedDeletedOrUpdated = FileSystemNotifications.FileNameChanged | FileSystemNotifications.FileModified;
        var waitable = FindFirstChangeNotification(directory.FullName, watchSubFolders, (uint)fileCreatedDeletedOrUpdated);
        var notifySize = Marshal.SizeOf(typeof(FileNotifyInformation));

        do
        {
            WaitForSingleObject(waitable, 0xFFFFFFFF); // Infinite wait
            var changes = new FileNotifyInformation[MaxChanges];
            var pinnedArray = GCHandle.Alloc(changes, GCHandleType.Pinned);
            var buffer = pinnedArray.AddrOfPinnedObject();
            uint bytesReturned;            

            ReadDirectoryChangesW(directoryHandle, buffer, (uint)(notifySize * MaxChanges), watchSubFolders, (uint)fileCreatedDeletedOrUpdated, out bytesReturned, IntPtr.Zero, IntPtr.Zero);

            for (var i = 0; i < bytesReturned / notifySize; i += 1)
            {
                var change = Marshal.PtrToStructure<FileNotifyInformation>(new IntPtr(buffer.ToInt64() + i * notifySize));

                if ((change.Action & (int)FileActions.FileAdded) == (int)FileActions.FileAdded)
                {
                    yield return new FileEvent(Marshal.PtrToStringAuto(change.FileName, change.FileNameLength), FileEventType.FileAdded);
                }
                else if ((change.Action & (int)FileActions.FileModified) == (int)FileActions.FileModified)
                {
                    yield return new FileEvent(Marshal.PtrToStringAuto(change.FileName, change.FileNameLength), FileEventType.FileChanged);
                }
                else if ((change.Action & (int)FileActions.FileRemoved) == (int)FileActions.FileRemoved)
                {
                    yield return new FileEvent(Marshal.PtrToStringAuto(change.FileName, change.FileNameLength), FileEventType.FileDeleted);
                }
                else if ((change.Action & (int)FileActions.FileRenamedNew) == (int)FileActions.FileRenamedNew)
                {
                    yield return new FileEvent(Marshal.PtrToStringAuto(change.FileName, change.FileNameLength), FileEventType.FileRenamed);
                }
            }

            pinnedArray.Free();
        } while (FindNextChangeNotification(waitable));

        FindCloseChangeNotification(waitable);
    }
}

var watcher = new DirectoryWatcher(@"C:\Temp");

foreach (var change in watcher.Watch())
{
    Console.WriteLine("File {0} was {1}", change.File.Name, change.EventType);
}

FileSystemWatcher(正如您已经注意到的那样)不可靠,您总是必须为丢失的文件添加一个 "custom"/手动逻辑(此外,请注意您可能会看到更多超过同一文件的一个事件)

您可以在下面看到一个简单的示例,其中包含 "background" 检查未处理的文件。
您可以通过使用并发集合来避免锁定,例如 BlockingCollection
您还可以选择并行处理文件
我正在根据计时器处理文件,但您可以使用自己的策略。
如果你不想实时处理文件,可能你甚至不需要 FileSystemWatcher

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace ConsoleAppDemo
{
    class Program
    {
        private static object lockIbj = new object();
        private static List<string> _proccessedFiles = new List<string>();
        private static readonly List<string> toProccessFiles = new List<string>();
        private static List<string> _proccessingFiles = new List<string>();
        private const string directory = @"C:\Path";
        private const string extension = @"*.txt";
        static void Main(string[] args)
        {
            FileSystemWatcher f = new FileSystemWatcher();
            f.Path = directory;
            f.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
                             | NotifyFilters.FileName | NotifyFilters.DirectoryName;
            f.Filter = extension ;
            f.Created += F_Created;
            f.EnableRaisingEvents = true;

            Timer manualWatcher = new Timer(ManuallWatcherCallback, null, 0, 3000);

            Timer manualTaskRunner = new Timer(ManuallRunnerCallback, null, 0, 10000);

            Console.ReadLine();
        }

        private static void F_Created(object sender, FileSystemEventArgs e)
        {
            lock (lockIbj)
            {
                toProccessFiles.Add(e.FullPath);
                Console.WriteLine("Adding new File from watcher: " + e.FullPath);
            }

        }

        private static void ManuallWatcherCallback(Object o)
        {
            var files = Directory.GetFiles(directory, extension);
            lock (lockIbj)
            {
                foreach (var file in files)
                {
                    if (!_proccessedFiles.Contains(file) && !toProccessFiles.Contains(file) && !_proccessingFiles.Contains(file))
                    {
                        toProccessFiles.Add(file);
                        Console.WriteLine("Adding new File from manuall timer: " + file);
                    }
                }

            }
        }

        private static bool processing;
        private static void ManuallRunnerCallback(Object o)
        {
            if (processing)
                return;

            while (true)
            {
                //you could proccess file in parallel
                string fileToProcces = null;

                lock (lockIbj)
                {
                    fileToProcces = toProccessFiles.FirstOrDefault();
                    if (fileToProcces != null)
                    {
                        processing = true;
                        toProccessFiles.Remove(fileToProcces);
                        _proccessingFiles.Add(fileToProcces);
                    }
                    else
                    {
                        processing = false;
                        break;


                    }
                }

                if (fileToProcces == null)
                    return;

                //Must add error handling
                ProccessFile(fileToProcces);
            }
        }

        private static void ProccessFile(string fileToProcces)
        {
            Console.WriteLine("Processing:" + fileToProcces);
            lock (lockIbj)
            {
                _proccessingFiles.Remove(fileToProcces);
                _proccessedFiles.Add(fileToProcces);
            }
        }
    }
}

我以前做过,但没有源代码(以前的工作),我 运行 遇到了同样的问题。我最终创建了一个 BackgroundWorker 实例来检查文件夹中是否有新文件。我会处理这些文件,然后将它们存档到一个子文件夹中。不确定是否有可能。

如果移动文件不是一个选项,BackgroundWorker 可能仍然是答案。跟踪文件的 LastModifiedDate 或 CreatedDate 并处理更新的日期。在您的 onCreated 中,您将创建一个 BackgroundWorker 的实例并将其 DoWork 放在文件上。由于您的处理需要 20 秒,我假设您在 onCreated 事件逻辑中直接调用了所有逻辑。通过将它带到另一个线程,您可以进行近乎即时的处理并完成,而另一个线程则在完成之前进行处理。