42

I want to read file continuously like GNU tail with "-f" param. I need it to live-read log file. What is the right way to do it?

Dmytro Leonenko
  • 1,443
  • 5
  • 19
  • 30

7 Answers7

43

More natural approach of using FileSystemWatcher:

    var wh = new AutoResetEvent(false);
    var fsw = new FileSystemWatcher(".");
    fsw.Filter = "file-to-read";
    fsw.EnableRaisingEvents = true;
    fsw.Changed += (s,e) => wh.Set();

    var fs = new FileStream("file-to-read", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    using (var sr = new StreamReader(fs))
    {
        var s = "";
        while (true)
        {
            s = sr.ReadLine();
            if (s != null)
                Console.WriteLine(s);
            else
                wh.WaitOne(1000);
        }
    }

    wh.Close();

Here the main reading cycle stops to wait for incoming data and FileSystemWatcher is used just to awake the main reading cycle.

tsul
  • 1,401
  • 1
  • 18
  • 22
  • 2
    You have to do: `fsw.EnableRaisingEvents = true;` before the `fsw.Changed` event is triggered – Mads Y Apr 28 '16 at 14:29
  • Yes, you are right, my mistake. The code above only works because of the `WaitOne` timeout. – tsul Apr 30 '16 at 23:43
  • 1
    @MadsY Fixed the answer as you proposed. – tsul Jun 14 '16 at 17:25
  • 5
    One caveat of file system watcher - it appears the file needs to be flushed before the event is raised. If you want real-time access to file changes, you need to open the file regularly and then set the file position to the end of the file, this will force a flush of any pending in-memory buffers, otherwise you are at the whim of the operating system to decide when it wants to flush. Normally not an issue unless you want any changes immediately. – jjxtra Dec 15 '19 at 18:14
  • 1
    @jixtra Wish I had read your comment 3 hours ago when I started trying to use `FileSystemWatcher`. I didn't see this point anywhere else. Thx – a113nw May 30 '20 at 22:56
41

You want to open a FileStream in binary mode. Periodically, seek to the end of the file minus 1024 bytes (or whatever), then read to the end and output. That's how tail -f works.

Answers to your questions:

Binary because it's difficult to randomly access the file if you're reading it as text. You have to do the binary-to-text conversion yourself, but it's not difficult. (See below)

1024 bytes because it's a nice convenient number, and should handle 10 or 15 lines of text. Usually.

Here's an example of opening the file, reading the last 1024 bytes, and converting it to text:

static void ReadTail(string filename)
{
    using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        // Seek 1024 bytes from the end of the file
        fs.Seek(-1024, SeekOrigin.End);
        // read 1024 bytes
        byte[] bytes = new byte[1024];
        fs.Read(bytes, 0, 1024);
        // Convert bytes to string
        string s = Encoding.Default.GetString(bytes);
        // or string s = Encoding.UTF8.GetString(bytes);
        // and output to console
        Console.WriteLine(s);
    }
}

Note that you must open with FileShare.ReadWrite, since you're trying to read a file that's currently open for writing by another process.

Also note that I used Encoding.Default, which in US/English and for most Western European languages will be an 8-bit character encoding. If the file is written in some other encoding (like UTF-8 or other Unicode encoding), It's possible that the bytes won't convert correctly to characters. You'll have to handle that by determining the encoding if you think this will be a problem. Search Stack overflow for info about determining a file's text encoding.

If you want to do this periodically (every 15 seconds, for example), you can set up a timer that calls the ReadTail method as often as you want. You could optimize things a bit by opening the file only once at the start of the program. That's up to you.

Chris F Carroll
  • 11,146
  • 3
  • 53
  • 61
Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • Why 1024? Why binnary? Example? – Dmytro Leonenko Sep 25 '10 at 10:09
  • If more or less then 1024 bytes were written since the last check, wouldn't this either miss some data or read some data twice? – Baruch Oct 29 '13 at 13:25
  • @baruch: Yes. But then, that's how the `tail` command works, too. – Jim Mischel Oct 29 '13 at 13:46
  • 2
    Worth noting, if the file is less than 1024 bytes, the code will throw an IOException because an attempt is made to move the file-pointer to a point before the start of the file. To avoid issue: fs.Seek(Math.Max(-1024,-fs.Length), SeekOrigin.End); – omglolbah Jan 04 '14 at 08:53
  • 12
    Tail doesn't blindly get the last 1024 bytes - it keeps track of the last size and the new size and only gets the bytes that were recently added. http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/tail.c – kah608 Feb 20 '14 at 22:00
  • 1
    @kah608: Thanks for the info. It should be easy enough to modify the code above to do that. – Jim Mischel Feb 20 '14 at 22:51
  • 1
    Downvoter: It's customary to leave an explanatory comment . . . – Jim Mischel May 14 '15 at 12:51
  • The other answer http://stackoverflow.com/a/24993767/3195477 seems simpler... is there an advantage to this approach? – StayOnTarget Apr 26 '17 at 11:36
7

To continuously monitor the tail of the file, you just need to remember the length of the file before.

public static void MonitorTailOfFile(string filePath)
{
    var initialFileSize = new FileInfo(filePath).Length;
    var lastReadLength = initialFileSize - 1024;
    if (lastReadLength < 0) lastReadLength = 0;

    while (true)
    {
        try
        {
            var fileSize = new FileInfo(filePath).Length;
            if (fileSize > lastReadLength)
            {
                using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    fs.Seek(lastReadLength, SeekOrigin.Begin);
                    var buffer = new byte[1024];

                    while (true)
                    {
                        var bytesRead = fs.Read(buffer, 0, buffer.Length);
                        lastReadLength += bytesRead;

                        if (bytesRead == 0)
                            break;

                        var text = ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead);

                        Console.Write(text);
                    }
                }
            }
        }
        catch { }

        Thread.Sleep(1000);
    }
}

I had to use ASCIIEncoding, because this code isn't smart enough to cater for variable character lengths of UTF8 on buffer boundaries.

Note: You can change the Thread.Sleep part to be different timings, and you can also link it with a filewatcher and blocking pattern - Monitor.Enter/Wait/Pulse. For me the timer is enough, and at most it only checks the file length every second, if the file hasn't changed.

Kind Contributor
  • 17,547
  • 6
  • 53
  • 70
  • 2
    Most log files will terminate with new line which you don't need to care about utf-8 encoding, so remembering the last new line position is really what you want – jjxtra Dec 15 '19 at 18:12
5

This is my solution:

Note this code simulates "tail -f -n +0". It means this code reads the whole file and continues to read the new lines. If you need to only read the new lines "tail -f -n 0", uncomment the reader.BaseStream.Seek(0, SeekOrigin.End) line.

    static IEnumerable<string> TailFrom(string file)
    {
        using (var reader = File.OpenText(file))
        {
            // go to end - if the next line is commented out, all the lines from the beginning is returned
            // reader.BaseStream.Seek(0, SeekOrigin.End);
            while (true) 
            {
                string line = reader.ReadLine();
                if (reader.BaseStream.Length < reader.BaseStream.Position) 
                    reader.BaseStream.Seek(0, SeekOrigin.Begin);

                if (line != null) yield return line;
                else Thread.Sleep(500);
            }
        }
    }

So, in your code you can do:

    foreach (string line in TailFrom(file)) 
    {
        Console.WriteLine($"line read= {line}");            
    }
hpaknia
  • 2,769
  • 4
  • 34
  • 63
iojancode
  • 610
  • 6
  • 7
  • best solution so far – domsch Mar 16 '22 at 16:15
  • This seems to loc the file and the application logging can no longer write to the file. Not sure if I am doing something wrong? – russelrillema Feb 10 '23 at 05:29
  • it opens the file for reading, it shouldn't lock. if issue still persist, you can try with StringReader(string, FileStreamOptions) instead of OpenText(string), to control FileAccess and FileShare options. – iojancode Feb 11 '23 at 15:02
2

You could use the FileSystemWatcher class which can send notifications for different events happening on the file system like file changed.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    OK. Let's say I received event that file changed. How can I read to the end from position I've ended reading last time? Eny example appreciated :) – Dmytro Leonenko Sep 25 '10 at 10:08
0
private void button1_Click(object sender, EventArgs e)
{
    if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
    {
        path = folderBrowserDialog.SelectedPath;
        fileSystemWatcher.Path = path;

        string[] str = Directory.GetFiles(path);
        string line;
        fs = new FileStream(str[0], FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        tr = new StreamReader(fs); 

        while ((line = tr.ReadLine()) != null)
        {

            listBox.Items.Add(line);
        }


    }
}

private void fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
    string line;
    line = tr.ReadLine();
    listBox.Items.Add(line);  
}
Zyberzero
  • 1,604
  • 2
  • 15
  • 35
coolcake
  • 2,917
  • 8
  • 42
  • 57
-9

If you are just looking for a tool to do this then check out free version of Bare tail

Snak3byte
  • 21
  • 7