如何检测文件缓冲区是否已被刷新

How to detect if a File buffer has been flushed

我正在尝试创建一个简单的日志记录工具来监视文件更改。我已经使用 FileSystemWatcher 来检测文件的更改,但我发现事件仅在文件关闭时触发,而不是在刷新缓冲区时触发。这意味着如果在文件关闭之前添加多行,我只会在文件关闭时看到。

这是我的测试示例。

[TestClass]
public class FileWriteTests
{

    [TestMethod]
    public void TestMethodAfterClose()
    {
        var currentDir = Environment.CurrentDirectory;
        var fileToMonitor = "test.txt";
        List<string> output = new List<string>();
        var watcherTest = new FileWatcherTest(fileToMonitor, currentDir, output);

        File.Delete(Path.Combine(currentDir, fileToMonitor));
        using (var writer = new StreamWriter(Path.Combine(currentDir, fileToMonitor), true))
        {
            writer.WriteLine($"test");
            writer.Flush();
        }
        System.Threading.Thread.Sleep(10);
        Assert.AreEqual(1, output.Count);
        Assert.AreEqual("test", output[0]);

    }

    [TestMethod]
    public void TestMethodAfterFlush()
    {
        var currentDir = Environment.CurrentDirectory;
        var fileToMonitor = "test.txt";
        List<string> output = new List<string>();
        var watcherTest = new FileWatcherTest(fileToMonitor, currentDir, output);

        File.Delete(Path.Combine(currentDir, fileToMonitor));

        using (var writer = new StreamWriter(Path.Combine(currentDir, fileToMonitor), true))
        {
            try
            {
                writer.WriteLine($"test");
                writer.Flush();
                System.Threading.Thread.Sleep(1000);
                // add break point here for BareTail
                Assert.AreEqual(1, output.Count);
                Assert.AreEqual("test", output[0]);
            }
            catch
            {
                Assert.Fail("Test failed");
            }
        }
    }

    public class FileWatcherTest
    {
        public string FileName { get; set; }
        public string Directory { get; set; }
        private List<string> linesRead;
        private FileSystemWatcher watcher;
        public FileWatcherTest(string fileName, string directory, List<string> output)
        {
            FileName = fileName;
            Directory = directory;
            linesRead = output;
            watcher = new FileSystemWatcher();
            watcher.Path = directory;
            watcher.Filter = FileName;
            watcher.Changed += Watcher_Changed;
            watcher.EnableRaisingEvents = true;
            watcher.NotifyFilter =  NotifyFilters.Attributes |
                                    NotifyFilters.CreationTime |
                                    NotifyFilters.DirectoryName |
                                    NotifyFilters.FileName |
                                    NotifyFilters.LastAccess |
                                    NotifyFilters.LastWrite |
                                    NotifyFilters.Security |
                                    NotifyFilters.Size;
        }

        private void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            using (var fileStream = File.Open(Path.Combine(Directory, FileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete | FileShare.Inheritable))
            {
                using (var reader = new StreamReader(fileStream))
                {
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        linesRead.Add(line);
                    }
                }
            }
        }
    }
}

现在 TestMethodAfterClose 成功,TestMethodAfterFlush 失败。当我使用程序 BareTail 并在断点处等待时,我看到它在文件关闭之前更新了显示。所以这给了我一个迹象,表明这是可能的。我不知道在 C# 中是否可行,我可能需要使用 dllimport 导入一些本机函数。问题是我不知道去哪里看

如何在不使用计时器的情况下使这两个测试都成功?

编辑: 更新了 FileWatcherTest class

不幸的是Flush没有刷新你想要的东西。我找了很多文章来解释它,例如:

https://blogs.msdn.microsoft.com/alejacma/2011/03/23/filesystemwatcher-class-does-not-fire-change-events-when-notifyfilters-size-is-used/

从.net 4开始就有解决方法,使用FileStream的另一种重载方法:Flush(bool)

var fs = writer.BaseStream as FileStream;
fs.Flush(true);

而且你只给磁盘 10ms 的反应时间,也许这是另一个问题。

经过一番搜索,我发现 FileSystemWatcher 仅在文件关闭后触发一个事件,如此 article 所示。文章只提到修改日期的 NotifyFilter,但在我的测试中我发现所有 Notifyfilters 在文件关闭后触发,而不会在它仍然打开时触发。

出于这个原因,看起来 拖尾 文件只能使用循环函数连续监视文件中的额外行。我以 link 上的代码为例。

这是我的代码:

[TestClass]
public class FileWriteTests
{

    [TestMethod]
    public void TestMethodAfterClose_filetailing()
    {

        var currentDir = Environment.CurrentDirectory;
        var fileToMonitor = "test.txt";
        File.Delete(Path.Combine(currentDir, fileToMonitor));
        List<string> output = new List<string>();
        using (var watcherTest = new PersonalFileTail(currentDir, fileToMonitor))
        {
            watcherTest.StartTail(delegate (string line) { output.Add(line); });
            using (var writer = new StreamWriter(Path.Combine(currentDir, fileToMonitor), true))
            {
                writer.WriteLine($"test");
                writer.Flush();
            }
            System.Threading.Thread.Sleep(200);
            watcherTest.StopTail();
        }
        System.Threading.Thread.Sleep(10);
        Assert.AreEqual(1, output.Count);
        Assert.AreEqual("test", output[0]);
    }

    [TestMethod]
    public void TestMethodAfterFlush_filetailing()
    {
        // initiate file
        var currentDir = Environment.CurrentDirectory;
        var fileToMonitor = "test.txt";
        File.Delete(Path.Combine(currentDir, fileToMonitor));
        FileInfo info = new FileInfo(Path.Combine(currentDir, fileToMonitor));

        List<string> output = new List<string>();
        using (var watcherTest = new PersonalFileTail(currentDir, fileToMonitor))
        {
            watcherTest.StartTail(delegate (string line) { output.Add(line); });
            using (var writer = new StreamWriter(Path.Combine(currentDir, fileToMonitor), true))
            {
                try
                {
                    writer.WriteLine($"test");
                    writer.Flush();
                    System.Threading.Thread.Sleep(1000);
                    Assert.AreEqual(1, output.Count);
                    Assert.AreEqual("test", output[0]);
                }
                catch
                {
                    Assert.Fail("Test failed");
                }
            }
            watcherTest.StopTail();
        }
    }

    public class PersonalFileTail : IDisposable
    {
        private string filename;
        private string directory;
        private Task fileTailTask;
        private Action<string> handleResults;
        private volatile bool runTask;
        private long lastFilePosition;
        public string FileName
        {
            get { return Path.Combine(directory, filename); }
        }
        public PersonalFileTail(string directory, string filename)
        {
            this.directory = directory;
            this.filename = filename;
            this.runTask = false;
            lastFilePosition = 0;
        }

        public void StartTail(Action<string> handleResults)
        {
            this.handleResults = handleResults;
            runTask = true;
            fileTailTask = Task.Run(() => MonitorFileTask());
        }

        public void StopTail()
        {
            runTask = false;
            fileTailTask.Wait();
        }

        public IEnumerable<string> ReadLinesFromFile()
        {
            using (StreamReader reader = new StreamReader(new FileStream(FileName,
            FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
            {
                string line = "";
                while ((line = reader.ReadLine()) != null)
                {
                    yield return line;
                }
                lastFilePosition = reader.BaseStream.Length;
            }
        }

        public void MonitorFileTask()
        {
            StreamReader reader = null;
            FileStream stream = null;
            try
            {
                using(stream = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                using (reader = new StreamReader(stream))
                {
                    do
                    {
                        //if the file size has increased do something
                        if (reader.BaseStream.Length > lastFilePosition)
                        {
                            //seek to the last max offset
                            reader.BaseStream.Seek(lastFilePosition, SeekOrigin.Begin);

                            //read out of the file until the EOF
                            string line = "";
                            while ((line = reader.ReadLine()) != null)
                            {
                                handleResults(line);
                            }

                            //update the last max offset
                            lastFilePosition = reader.BaseStream.Position;
                        }
                        // sleep task for 100 ms 
                        System.Threading.Thread.Sleep(100);
                    }
                    while (runTask);
                }
            }
            catch
            {
                if (reader != null)
                    reader.Dispose();
                if (stream != null)
                    stream.Dispose();
            }
        }

        public void Dispose()
        {
            if(runTask)
            {
                runTask = false;
                fileTailTask.Wait();
            }
        }
    }
}

如果有人知道不使用定时函数就可以完成拖尾的方法,我会接受它作为答案。直到那个时候,我觉得我的回答是唯一可行的方法。