Created
November 9, 2015 16:03
-
-
Save gyulalaszlo/8df3f597feab3f2cbe16 to your computer and use it in GitHub Desktop.
C# log file watcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Threading; | |
using System.IO; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
/// <summary> | |
/// | |
/// </summary> | |
/// <example> | |
/// const string path = @"c:\Users\Miles\Documents\log-tests"; | |
/// Watcher.startWatching( | |
/// new string[] { path, path + "\\Logs" }, | |
/// 5000, | |
/// (string file, string[] lines) => | |
/// { | |
/// Console.WriteLine("Got change in:" + file); | |
/// foreach (var line in lines) { Console.WriteLine(" -> " + line); } | |
/// }); | |
/// </example> | |
namespace LogWatcher | |
{ | |
/// <summary> | |
/// Utility class for working with existing log files | |
/// </summary> | |
class LogFile | |
{ | |
public string path; | |
public long lastSize; | |
public LogFile(string path) | |
{ | |
this.path = path; | |
UpdateLastSize(this); | |
} | |
/// <summary> | |
/// Reads any new lines at the end of the file | |
/// </summary> | |
/// <returns>Null if no changes since last try, or the appended lines if there are any.</returns> | |
public string[] read() | |
{ | |
// Figure out the new size | |
var newSize = new FileInfo(path).Length; | |
// If the sizes match return nada | |
if (newSize == lastSize) return null; | |
// If the sizes differ, find out the target offset. | |
var seekOffset = (newSize > lastSize) ? lastSize : 0; | |
UpdateLastSize(this); | |
return ReadFromOffset(path, seekOffset); | |
} | |
/// <summary> | |
/// Returns the contents of the file at path from the byte offset | |
/// seekoffset | |
/// </summary> | |
/// <param name="path">The full path of the file</param> | |
/// <param name="seekOffset">The offset to seek to</param> | |
/// <returns></returns> | |
private static string[] ReadFromOffset(string path, long seekOffset) | |
{ | |
// Open the stream and reader (then close them) | |
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) | |
using (var reader = new StreamReader(fs)) | |
{ | |
// Seek to the end or other start position | |
reader.BaseStream.Seek(seekOffset, SeekOrigin.Begin); | |
// Read the lines into a buffer | |
var lines = new List<string>(); | |
var line = ""; | |
while ((line = reader.ReadLine()) != null) | |
lines.Add(line); | |
return lines.ToArray(); | |
} | |
} | |
/// <summary> | |
/// Updates the last size field of this logfile | |
/// </summary> | |
private static void UpdateLastSize(LogFile f) | |
{ | |
f.lastSize = new FileInfo(f.path).Length; | |
} | |
} | |
} | |
namespace LogWatcher | |
{ | |
// We need to double namespace it so this using declaration sits where it supposed to sit, on | |
// the first row of the namespace. | |
using FileStreamDict = Dictionary<string, LogFile>; | |
class Watcher | |
{ | |
/// <summary> | |
/// Starts a blocking watcher for files provided by config. | |
/// </summary> | |
/// <param name="directories">The directories to watch</param> | |
/// <param name="cycleLength">The amount of milliseconds to wait in between the check attempts.</param> | |
/// <param name="onChange"> | |
/// A callback action / lambda that gets called on change events. | |
/// Parameters: (string filename, string[] lines) | |
/// </param> | |
public static void startWatching(string[] directories, int cycleLength, Action<string, string[]> onChange) | |
{ | |
// Get a list of all files in the directory | |
var streams = ChangeCycle(directories, new FileStreamDict(), (string file, string[] lines) => { }); | |
while (true) | |
{ | |
streams = ChangeCycle(directories, streams, onChange); | |
Thread.Sleep(cycleLength); | |
} | |
} | |
/// <summary> | |
/// Do a single check cycle. | |
/// </summary> | |
/// <param name="directories">The directories to check</param> | |
/// <param name="existingFiles">A list of LogFiles already added to the watcher</param> | |
/// <param name="onChange">The delegate to callback</param> | |
/// <returns></returns> | |
private static FileStreamDict ChangeCycle(string[] directories, FileStreamDict existingFiles, Action<string, string[]> onChange) | |
{ | |
var streams = addNewFilesToStreams(directories, existingFiles); | |
// Try to read from each file stream | |
foreach (KeyValuePair<string, LogFile> entry in streams) | |
{ | |
var lines = entry.Value.read(); | |
if (lines != null) | |
// Trigger the change handler | |
onChange(entry.Key, lines); | |
} | |
return streams; | |
} | |
/// <summary> | |
/// Returns a new map of filename/stream maps | |
/// </summary> | |
/// <param name="cfg"></param> | |
/// <param name="existingFiles"></param> | |
/// <returns></returns> | |
private static FileStreamDict addNewFilesToStreams(string[] directories, FileStreamDict existingFiles) | |
{ | |
// Mapcat the files into a single list | |
var files = directories.Select(path => Directory.GetFiles(path)).SelectMany(x => x); | |
var outMap = new FileStreamDict(); | |
foreach (var file in files) | |
outMap[file] = existingFiles.ContainsKey(file) ? existingFiles[file] : new LogFile(file); | |
return outMap; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
sdas