FileSystemWatcher的Changed事件被触发两次

390

我有一个应用程序,在其中寻找一个文本文件,如果文件有任何更改,我将使用 OnChanged 事件处理程序来处理该事件。我正在使用 NotifyFilters.LastWriteTime 但是仍然会触发两次事件。以下是代码。

public void Initialize()
{
   FileSystemWatcher _fileWatcher = new FileSystemWatcher();
  _fileWatcher.Path = "C:\\Folder";
  _fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
  _fileWatcher.Filter = "Version.txt";
  _fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
  _fileWatcher.EnableRaisingEvents = true;
}

private void OnChanged(object source, FileSystemEventArgs e)
{
   .......
}
在我的情况下,当我改变文本文件version.txt并保存它时,OnChanged被调用了两次。

这是一个解决方法,但应该根据解决方法的质量来评判。跟踪更改效果完美且简单。OP正在寻找一种抑制重复事件的方法,下面的回复就提供了这种方法。https://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.created.aspx解释了多个事件可能是由于反病毒软件或其他“复杂的文件系统问题”引起的(听起来只是借口)。 - Tyler Montney
2
我最近打开了这个问题 https://github.com/Microsoft/dotnet/issues/347 - Stephan Ahlf
3
我创建了一个类,可以帮助您获取一个事件。您可以从https://github.com/melenaos/FileSystemSafeWatcher获取代码。 - Menelaos Vergis
上面Menelaos Vergis提供的解决方案百分之百有效。 - undefined
45个回答

1
抱歉打扰了,但我已经与这个问题斗争了一段时间,最终找到了处理多次触发事件的方法。我想感谢这个帖子中的每个人,因为在解决这个问题时我参考了它。
以下是我的完整代码。它使用字典来跟踪文件上次写入的日期和时间。它比较该值,如果相同,则抑制事件。然后在启动新线程后设置该值。
using System.Threading; // used for backgroundworker
using System.Diagnostics; // used for file information
private static IDictionary<string, string> fileModifiedTable = new Dictionary<string, string>(); // used to keep track of our changed events

private void fswFileWatch_Changed( object sender, FileSystemEventArgs e )
    {
        try
        {
           //check if we already have this value in our dictionary.
            if ( fileModifiedTable.TryGetValue( e.FullPath, out sEmpty ) )
            {              
                //compare timestamps      
                if ( fileModifiedTable[ e.FullPath ] != File.GetLastWriteTime( e.FullPath ).ToString() )
                {        
                    //lock the table                
                    lock ( fileModifiedTable )
                    {
                        //make sure our file is still valid
                        if ( File.Exists( e.FullPath ) )
                        {                               
                            // create a new background worker to do our task while the main thread stays awake. Also give it do work and work completed handlers
                            BackgroundWorker newThreadWork = new BackgroundWorker();
                            newThreadWork.DoWork += new DoWorkEventHandler( bgwNewThread_DoWork );
                            newThreadWork.RunWorkerCompleted += new RunWorkerCompletedEventHandler( bgwNewThread_RunWorkerCompleted );

                            // capture the path
                            string eventFilePath = e.FullPath;
                            List<object> arguments = new List<object>();

                            // add arguments to pass to the background worker
                            arguments.Add( eventFilePath );
                            arguments.Add( newEvent.File_Modified );

                            // start the new thread with the arguments
                            newThreadWork.RunWorkerAsync( arguments );

                            fileModifiedTable[ e.FullPath ] = File.GetLastWriteTime( e.FullPath ).ToString(); //update the modified table with the new timestamp of the file.
                            FILE_MODIFIED_FLAG.WaitOne(); // wait for the modified thread to complete before firing the next thread in the event multiple threads are being worked on.
                        }
                    }
                }
            }
        }
        catch ( IOException IOExcept )
        {
            //catch any errors
            postError( IOExcept, "fswFileWatch_Changed" );
        }
    }

无法正常工作,因为触发的事件间隔太短:最后写入时间:636076274162565607 最后写入时间:636076274162655722。 - Professor of programming

1
你可以尝试以写模式打开它,如果成功了,那么你就可以假设其他应用程序已经完成了对该文件的操作。
private void OnChanged(object source, FileSystemEventArgs e)
{
    try
    {
        using (var fs = File.OpenWrite(e.FullPath))
        {
        }
        //do your stuff
    }
    catch (Exception)
    {
        //no write access, other app not done
    }
}

仅仅打开它进行写操作似乎不会触发更改事件。因此,这应该是安全的。


1
FileReadTime = DateTime.Now;

private void File_Changed(object sender, FileSystemEventArgs e)
{            
    var lastWriteTime = File.GetLastWriteTime(e.FullPath);
    if (lastWriteTime.Subtract(FileReadTime).Ticks > 0)
    {
        // code
        FileReadTime = DateTime.Now;
    }
}

1
虽然这可能是问题的最佳解决方案,但最好还是添加一些评论,说明您选择了这种方法以及为什么您认为它可行。 :) - waka

1
即使没有被要求,没有现成的F#解决方案示例仍然是一件遗憾的事情。 为了解决这个问题,这里提供了我的方法,只是因为我能够做到,并且F#是一个很棒的.NET语言。
使用包过滤重复事件,该包只是反应扩展的F#封装器。所有这些都可以针对完整框架或进行定位:
let createWatcher path filter () =
    new FileSystemWatcher(
        Path = path,
        Filter = filter,
        EnableRaisingEvents = true,
        SynchronizingObject = null // not needed for console applications
    )

let createSources (fsWatcher: FileSystemWatcher) =
    // use here needed events only. 
    // convert `Error` and `Renamed` events to be merded
    [| fsWatcher.Changed :> IObservable<_>
       fsWatcher.Deleted :> IObservable<_>
       fsWatcher.Created :> IObservable<_>
       //fsWatcher.Renamed |> Observable.map renamedToNeeded
       //fsWatcher.Error   |> Observable.map errorToNeeded
    |] |> Observable.mergeArray

let handle (e: FileSystemEventArgs) =
    printfn "handle %A event '%s' '%s' " e.ChangeType e.Name e.FullPath 

let watch path filter throttleTime =
    // disposes watcher if observer subscription is disposed
    Observable.using (createWatcher path filter) createSources
    // filter out multiple equal events
    |> Observable.distinctUntilChanged
    // filter out multiple Changed
    |> Observable.throttle throttleTime
    |> Observable.subscribe handle

[<EntryPoint>]
let main _args =
    let path = @"C:\Temp\WatchDir"
    let filter = "*.zip"
    let throttleTime = TimeSpan.FromSeconds 10.
    use _subscription = watch path filter throttleTime
    System.Console.ReadKey() |> ignore
    0 // return an integer exit code

1
在我的情况下,需要获取由其他应用程序插入的文本文件的最后一行,一旦插入完成。这是我的解决方案。当第一个事件被触发时,我禁用了监视器以避免再次触发,然后调用计时器TimeElapsedEvent,因为当我的处理函数OnChanged被调用时,我需要文本文件的大小,但此时的大小不是实际大小,而是插入之前的大小。所以我等待一段时间才能使用正确的文件大小继续进行。
private FileSystemWatcher watcher = new FileSystemWatcher();
...
watcher.Path = "E:\\data";
watcher.NotifyFilter = NotifyFilters.LastWrite ;
watcher.Filter = "data.txt";
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = true;

...

private void OnChanged(object source, FileSystemEventArgs e)
   {
    System.Timers.Timer t = new System.Timers.Timer();
    try
    {
        watcher.Changed -= new FileSystemEventHandler(OnChanged);
        watcher.EnableRaisingEvents = false;

        t.Interval = 500;
        t.Elapsed += (sender, args) => t_Elapsed(sender, e);
        t.Start();
    }
    catch(Exception ex) {
        ;
    }
}

private void t_Elapsed(object sender, FileSystemEventArgs e) 
   {
    ((System.Timers.Timer)sender).Stop();
       //.. Do you stuff HERE ..
     watcher.Changed += new FileSystemEventHandler(OnChanged);
     watcher.EnableRaisingEvents = true;
}

1

跳过重复数据太冒险了,因为代码可能会看到不完整的数据版本。

相反,我们可以等待指定的合并毫秒数,直到没有更改为止。

重要提示:下面的示例仅适用于当您想要在一个或多个文件更改时收到单个通知的情况。其中一个原因是它硬编码了NotifyFilter,这可以修改以允许更改。另一个原因是它不告诉您哪些文件已更改或更改的类型(FileSystemEventArgs),这也可以修改以提供检测到的所有更改列表。

一个很大的缺点是,如果文件更新的频率超过合并毫秒数,则不会触发

private sealed class SingleFireFilesWatcher : IDisposable
{
    public event EventHandler? Changed;
    private readonly FileSystemWatcher watcher;
    private bool disposed;
    public SingleFireFilesWatcher(int mergeMilliseconds, params string[] filters)
    {
        var timer = new Timer(_ => Changed?.Invoke(this, EventArgs.Empty));
        watcher = new FileSystemWatcher();
        foreach (var filter in filters)
            watcher.Filters.Add(filter);
        watcher.NotifyFilter = NotifyFilters.LastWrite;
        watcher.Changed += (s, e) => timer.Change(mergeMilliseconds, Timeout.Infinite);
        watcher.EnableRaisingEvents = true;
    }

    public void Dispose()
    {
        if (disposed) return;
        disposed = true;
        watcher.Dispose();
    }
}

解决上述限制的方法是始终在第一次更改后触发合并毫秒。代码只稍微复杂一些。
其中一个缺点是,如果mergeMilliseconds设置得非常低,您可能会获得大量额外的触发。
private sealed class SingleFireFilesWatcher2 : IDisposable
{
    public event EventHandler? Changed;
    private readonly FileSystemWatcher watcher;
    private bool disposed;
    public SingleFireFilesWatcher2(int mergeMilliseconds, params string[] filters)
    {
        var restartTimerOnNextChange = true;
        var timer = new Timer(_ =>
        {
            restartTimerOnNextChange = true;
            Changed?.Invoke(this, EventArgs.Empty);
        });
        watcher = new FileSystemWatcher();
        foreach (var filter in filters)
            watcher.Filters.Add(filter);
        watcher.NotifyFilter = NotifyFilters.LastWrite;
        watcher.Changed += (s, e) =>
        {
            if (restartTimerOnNextChange)
                timer.Change(mergeMilliseconds, Timeout.Infinite);
            restartTimerOnNextChange = false;
        };
        watcher.EnableRaisingEvents = true;
    }

    public void Dispose()
    {
        if (disposed) return;
        disposed = true;
        watcher.Dispose();
    }
}

FileSystemEventArgsеҸӮж•°жІЎжңүдј ж’ӯпјҢиҝҷдёҚжҳҜдёҖдёӘй—®йўҳеҗ—пјҹ - Theodor Zoulias
@TheodorZoulias 这取决于使用情况。如果这样做,它将只是最后一个更改的参数或所有接收到的参数列表。 - eglasius
@TheodorZoulias 顺便提一下,针对我的使用情况,我只关心一个或多个文件是否正在更改,并且能够在更改生效后做出反应。 - eglasius
1
你可以考虑在答案中提及这个限制,这样人们就知道它是否适合他们的情况。 - Theodor Zoulias

0
我只是简单地添加了一个重复检查,如下所示:
 private void OnChanged(object source, FileSystemEventArgs e)
    {
        string sTabName = Path.GetFileNameWithoutExtension(e.Name);
        string sLastLine = ReadLastLine(e.FullPath);
        if(sLastLine != _dupeCheck)
        {
            TabPage tp = tcLogs.TabPages[sTabName];
            TextBox tbLog = (TextBox)tp.Controls[0] as TextBox;

            tbLog.Invoke(new Action(() => tbLog.AppendText(sLastLine + Environment.NewLine)));
            tbLog.Invoke(new Action(() => tbLog.SelectionStart = tbLog.Text.Length));
            tbLog.Invoke(new Action(() => tbLog.ScrollToCaret()));
            _dupeCheck = sLastLine;
        }
    }

    public static String ReadLastLine(string path)
    {
        return ReadLastLine(path, Encoding.Default, "\n");
    }

    public static String ReadLastLine(string path, Encoding encoding, string newline)
    {
        int charsize = encoding.GetByteCount("\n");
        byte[] buffer = encoding.GetBytes(newline);
        using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            long endpos = stream.Length / charsize;
            for (long pos = charsize; pos < endpos; pos += charsize)
            {
                stream.Seek(-pos, SeekOrigin.End);
                stream.Read(buffer, 0, buffer.Length);
                if (encoding.GetString(buffer) == newline)
                {
                    buffer = new byte[stream.Length - stream.Position];
                    stream.Read(buffer, 0, buffer.Length);
                    return encoding.GetString(buffer);
                }
            }
        }
        return null;
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);

    private const int WM_VSCROLL = 0x115;
    private const int SB_BOTTOM = 7;

    /// <summary>
    /// Scrolls the vertical scroll bar of a multi-line text box to the bottom.
    /// </summary>
    /// <param name="tb">The text box to scroll</param>
    public static void ScrollToBottom(TextBox tb)
    {
        SendMessage(tb.Handle, WM_VSCROLL, (IntPtr)SB_BOTTOM, IntPtr.Zero);
    }

0
解决方案实际上取决于使用情况。您是在观察不变的新文件,还是一个偶尔更改或经常更改的文件?在我的情况下,它不会经常更改,我不想错过任何这些更改。
但我也不希望在写入过程尚未完成时发生更改事件。
在我的情况下,我注意到在编写125个字符的txt文件时有6个(六个!!)onchange事件。
我的解决方案是轮询和更改事件的混合,通常被认为是负面的。正常轮询很慢,比如每10秒钟一次,以防FileSystemWatcher(FSW)“错过”事件。轮询立即响应FSW更改事件。
诀窍在于,在FSW.Change事件中,轮询速度更快,比如每100毫秒,并等待文件稳定。因此,我们有了“两个阶段的轮询”:第一阶段很慢,但会立即响应FSW文件更改事件。第二阶段很快,等待稳定的文件。

如果FSW检测到多个文件更改事件,每个事件都会加速轮询循环,并有效地启动一个新的、短暂的等待周期。只有在轮询循环检测到文件上次写入时间没有进一步更改时,它才认为该文件是稳定的,您的代码可以处理已更改的文件。

我选择了10秒和100毫秒的超时时间,但您的用例可能需要不同的超时值。

这里是轮询的代码,其中AppConfig.fiIO是要监视的FileInfo

private readonly EventWaitHandle ewhTimeout = new AutoResetEvent(false);

private void TwoPhasedPolling()
{
    bool WaitForChange = true; //false: wait until stable
    DateTime LastWriteTime = DateTime.MinValue;
    while (true)
    {
        // wait for next poll (timeout), or FSW event
        bool GotOne = ewhTimeout.WaitOne(WaitForChange ? 10 * 1000 : 100);
        if (GotOne)
        {
            // WaitOne interrupted: end of Phase1: FSW detected file change
            WaitForChange = false;
        }
        else
        {
            // WaitOne timed out: Phase2: check file write time for change
            if (AppConfig.fiIO.LastWriteTime > LastWriteTime)
            {
                LastWriteTime = AppConfig.fiIO.LastWriteTime;
            }
            else
            {
                // End of Phase2: file has changed and is stable
                WaitForChange = true;
                // action on changed file
                ... your code here ...
            }}}}

private void fileSystemWatcher1_Changed(object sender, FileSystemEventArgs e)
{
    ewhTimeout.Set();
}

NB:是的,我也不喜欢 }}}},但它可以使列表更短,这样您就不必滚动 :-)

你的代码在Visual Studio中是否会产生一个关于缺少await关键字的async方法警告?另外,我认为你应该提到你的解决方案是用于监视单个文件的。 - Theodor Zoulias
@TheodorZoulias 既然你提到它,确实是这样。请随意改进我的答案,但我认为此算法是解决“更改事件被触发两次”的问题最纯粹的方法,第一次尝试即可成功,并且我不认为缺少await会有什么问题,除了伤害VS的感情。请将您的踩投给“没有用处”的答案,查看下箭头上的鼠标弹出提示。 - Roland
@TheodorZoulias,我确实提到了单文件用例。原始问题也是这样。 - Roland
在我看来,Roland提到的单文件用法并不够清晰/突出。至于改进他人的答案,生命太短了。在我看来,我的否决是公正的,因为即使你的答案展示了如何解决实际问题的想法,它也促进了不良编码实践。表现良好的异步方法不应该阻塞调用者。它们应该返回一个未完成的“Task”,让调用者可以异步等待。您可以通过使用“SemaphoreSlim”替换“EventWaitHandle”来修复此缺陷。这有一个“WaitAsync”方法,可以被异步等待。 - Theodor Zoulias
@TheodorZoulias 这只是你的观点。第一句话讨论了使用情况,而其他答案完全避免了这个问题。我感谢你对信号量的建议,我会去了解一下。但即使没有信号量,我发现这个解决方案非常有用和原创,我想分享它。我没有遇到调用者被阻塞的情况。为了使这个答案对其他人更有用,我删除了Task的实现细节,这与算法无关。请重新考虑你的反对。 - Roland

0

虽然有点晚,但我最近遇到这个问题,因此我想做出一点贡献。

首先,许多提出的解决方案似乎只适用于单个更新文件,而我需要在短时间内(毫秒级)通知2-3个更改文件,在相对较长的重复时间(几十秒到几分钟)内进行通知。

最有趣的早期建议链接之一是FileSystemWatcher is a Bit Broken。然而,正如同一作者在Erratic Behaviour from .NET MemoryCache Expiration Demystified中所指出的那样,所提出的解决方案只部分地起作用,即使在20秒后也会发出通知。

那么我所做的就是基于类似原理的愚蠢替代方案,没有使用MemoryCache

基本上,它创建了一个带有完整文件路径和过期计时器的项目List<>。如果另一个事件再次触发更改,则在列表中找到该元素并使用新的过期时间更新计时器。

过期时间经验性地足够长,可以在单个OnStableChange通知中收集多个事件,而不会感觉反应迟钝。

当您实例化Whatever时,还将其与目录和非常基本的外部回调Link在一起。

没有真正优化,我只是在几行代码中寻找解决方案

我在这里发布它,因为:

  1. 对我来说,它可以在另一个应用程序上进行验证
  2. 更聪明、更有经验的人可以改进它,并帮助我理解它在哪些方面不够健壮
    internal class Whatever
    {
        private FileSystemWatcher? watcher = null;

        public delegate void DelegateFileChange(string path);
        public DelegateFileChange? onChange;

        private const int CacheTimeMilliseconds = 200;

        private class ChangeItem
        {
            public delegate void DelegateChangeItem(string key);
            public string Key { get; set; } = "";
            public System.Timers.Timer Expiration = new();
            public DelegateChangeItem? SignalChanged = null;
        }
        private class ChangeCache
        {
            private readonly List<ChangeItem> _changes = new();

            public void Set(string key, int milliSecs, ChangeItem.DelegateChangeItem? signal = null)
            {
                lock (_changes)
                {
                    ChangeItem? existing = _changes.Find(item => item.Key == key);
                    if (existing != null)
                    {
                        existing.Expiration.Interval = milliSecs;
                        existing.SignalChanged = signal;
                    }
                    else
                    {
                        ChangeItem change = new()
                        {
                            Key = key,
                            SignalChanged = signal
                        };
                        change.Expiration.Interval = milliSecs;
                        change.Expiration.AutoReset = false;
                        change.Expiration.Elapsed += delegate { Change_Elapsed(key); };
                        change.Expiration.Enabled = true;
                        _changes.Add(change);
                    }
                }
            }

            private void Change_Elapsed(string key)
            {
                lock (_changes)
                {
                    ChangeItem? existing = _changes.Find(item => item.Key == key);
                    existing?.SignalChanged?.Invoke(key);
                    _changes.RemoveAll(item => item.Key == key);
                }
            }
        }

        private ChangeCache changeCache = new();

        public bool Link(string directory, DelegateFileChange? fileChange = null)
        {
            bool result = false;

            try
            {
                if (Directory.Exists(directory))
                {
                    watcher = new FileSystemWatcher(directory);
                    watcher.NotifyFilter = NotifyFilters.LastWrite;
                    watcher.Changed += Watcher_Changed;

                    onChange = fileChange;

                    watcher.Filter = "*.*";
                    watcher.IncludeSubdirectories = true;
                    watcher.EnableRaisingEvents = true;

                    result = true;
                }
            }
            catch (Exception)
            {
            }

            return result;
        }

        private void OnStableChange(string path)
        {
            if (File.Exists(path))
            {
                onChange?.Invoke(path);
            }
        }

        public void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            changeCache.Set(e.FullPath, CacheTimeMilliseconds, OnStableChange);
        }
    }

0

我在此将这段代码遗留给未来的后代:

    static DateTimeOffset lastChanged = DateTimeOffset.UtcNow;
        static string lastChangedFile = null;

...

        private static void OnChanged(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType != WatcherChangeTypes.Changed || 
                (lastChanged.AddMilliseconds(500) > DateTimeOffset.UtcNow && lastChangedFile == e.FullPath)
               ) 
            {
                return;
            }
            lastChanged = DateTimeOffset.UtcNow;
            lastChangedFile = e.FullPath;
            Console.WriteLine($"Changed: {e.FullPath}");
            
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接