C# 和 FileSystemWatcher

C# and the FileSystemWatcher

我用 C# 编写了一个服务,它应该将备份文件(*.bak 和 *.trn)从数据库服务器移动到一个特殊的备份服务器。到目前为止,这工作得很好。问题是它试图将单个文件移动两次。这当然失败了。我已按如下方式配置 FileSystemWatcher:

try
{
    m_objWatcher = new FileSystemWatcher();
    m_objWatcher.Filter = m_strFilter;
    m_objWatcher.Path = m_strSourcepath.Substring(0, m_strSourcepath.Length - 1);
    m_objWatcher.IncludeSubdirectories = m_bolIncludeSubdirectories;
    m_objWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess; // | NotifyFilters.CreationTime;
    m_objWatcher.Changed += new FileSystemEventHandler(objWatcher_OnCreated);
}
catch (Exception ex)
{
    m_objLogger.d(TAG, m_strWatchername + "InitFileWatcher(): " + ex.ToString());
}

Watcher是否可能为同一个文件两次产生一个事件?如果我将过滤器设置为仅 CreationTime,它根本没有反应。

我如何才能将 Watcher 设置为每个文件只触发一次事件?

在此先感谢您的帮助

The documentation 指出常见的文件系统操作可能引发不止一个事件。在“事件和缓冲区大小”标题下查看。

Common file system operations might raise more than one event. For example, when a file is moved from one directory to another, several OnChanged and some OnCreated and OnDeleted events might be raised. Moving a file is a complex operation that consists of multiple simple operations, therefore raising multiple events. Likewise, some applications (for example, antivirus software) might cause additional file system events that are detected by FileSystemWatcher.

它还提供了一些指南,包括:

Keep your event handling code as short as possible.

为此,您可以使用 FileSystemWatcher.Changed 事件对 queue 文件进行处理,然后再处理它们。这是一个快速示例,说明使用 System.Threading.Timer 的实例处理 queue.

可能是什么样子
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;

public class ServiceClass
{
    public ServiceClass()
    {
        _processing = false;
        _fileQueue = new ConcurrentQueue<string>();
        _timer = new System.Threading.Timer(ProcessQueue);
        // Schedule the time to run in 5 seconds, then again every 5 seconds.
        _timer.Change(5000, 5000);
    }

    private void objWatcher_OnChanged(object sender, FileSystemEventArgs e)
    {
        // Just queue the file to be processed later. If the same file is added multiple
        // times, we'll skip the duplicates when processing the files.
        _fileQueue.Enqueue(e.FilePath);
    }

    private void ProcessQueue(object state)
    {
        if (_processing)
        {
            return;
        }
        _processing = true;
        var failures = new HashSet<string>();
        try
        {
            while (_fileQueue.TryDequeue(out string fileToProcess))
            {
                if (!File.Exists(fileToProcess))
                {
                    // Probably a file that was added multiple times and it was
                    // already processed.
                    continue; 
                }
                var file = new FileInfo(fileToProcess);
                if (FileIsLocked(file))
                {
                    // File is locked. Maybe you got the Changed event, but the file
                    // wasn't done being written.
                    failures.Add(fileToProcess);
                    continue;
                }
                try
                {
                    fileInfo.MoveTo(/*Your destination*/);
                }
                catch (Exception)
                {
                    // File failed to move. Add it to the failures so it can be tried
                    // again.
                    failutes.Add(fileToProcess);
                }
            }
        }
        finally
        {
            // Add any failures back to the queue to try again.
            foreach (var failedFile in failures)
            {
                _fileQueue.Enqueue(failedFile);
            }
            _processing = false;
        }
    }

    private bool IsFileLocked(FileInfo file)
    {
        try
        {
            using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read,
               FileShare.None))
            {
                stream.Close();
            }
        }
        catch (IOException)
        {
            return true;
        }
        return false;
    }

    private System.Threading.Timer _timer;
    private bool _processing;
    private ConcurrentQueue<string> _fileQueue;
}

功劳归功于我,我从 this answer 中获得了 FileIsLocked

您可能需要考虑的其他一些事项:

如果您的 FileSystemWatcher 错过了活动会怎样? [文档] 确实说明这是可能的。

Note that a FileSystemWatcher may miss an event when the buffer size is exceeded. To avoid missing events, follow these guidelines:

Increase the buffer size by setting the InternalBufferSize property.

Avoid watching files with long file names, because a long file name contributes to filling up the buffer. Consider renaming these files using shorter names.

Keep your event handling code as short as possible.

如果您的服务崩溃,但写入备份文件的进程继续写入它们,会发生什么情况?当您重新启动服务时,它会选择这些文件并移动它们吗?

我尝试了各种想法来阻止它。事件靠得太近了……无法在 FileChanged 事件中停止。这是我的工作解决方案:

    private System.Timers.Timer timer;
    private FileSystemWatcher fwatcher;

    static void Main(string[] args)
    {
        new Program();
    }

    private Program()
    {
        timer = new System.Timers.Timer(100);
        timer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
        timer.AutoReset = false; // only once

        fwatcher = new FileSystemWatcher();
        fwatcher.Path = filePath;
        fwatcher.Filter = fileName;
        fwatcher.NotifyFilter = NotifyFilters.LastWrite;
        fwatcher.Changed += new FileSystemEventHandler(FileChanged);
        fwatcher.EnableRaisingEvents = true;

        while (IsRunning)
        {
            Thread.Sleep(100);
        }
        Thread.Sleep(100);
    }
    
    private void FileChanged(object sender, FileSystemEventArgs e)
    {
        timer.Start();
    }

    private void OnTimedEvent(object source, ElapsedEventArgs e)
    {
        Console.WriteLine("file has changed!");
    }

每次更改文件时计时器只会触发一次。