Simple syncronized IO logger for CLI apps
namespace Sample
{
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// This class allows multiple threads to write without blocking and serializes the writes to a single
/// consumer thread.
/// </summary>
public class SynchronizedIO<T> : IDisposable
{
Action<T> writer;
ConcurrentQueue<T> queue;
Task task;
bool quit;
AutoResetEvent signal;
public SynchronizedIO(Action<T> writer)
{
this.writer = writer;
this.queue = new ConcurrentQueue<T>();
this.signal = new AutoResetEvent(false);
this.task = new Task(MonitorQueue);
this.task.Start();
}
public void Enqueue(T data)
{
// add data to queue
queue.Enqueue(data);
// signal the monitor thread that there is work to be done
signal.Set();
}
/// <summary>
/// Finish any pending writes and then close the streams.
/// </summary>
public void Dispose()
{
// Signal the monitor thread that there is work to be done and that
// it should exit.
quit = true;
signal.Set();
// wait until it finishes
task.Wait();
}
/// <summary>
/// Synchronously commit any queued data.
/// </summary>
public void Flush()
{
while (queue.Count > 0)
{
signal.Set();
Thread.Sleep(5);
}
}
private void MonitorQueue()
{
while (true)
{
// wait for work to show up
signal.WaitOne(TimeSpan.FromSeconds(3));
{
// process each data block in the queue
T data;
while (queue.TryDequeue(out data))
{
// write the data
this.writer(data);
}
}
if (quit && queue.Count == 0)
{
break;
}
// Prevent very "chatty" waits. It's ok to accumulate writes as long as we can keep up
// the burst of writes. Otherwise single writes will result in kernel transitions most times
// Could also use a lighter wait algorithm (SpinLocks / Monitor.Pulse or similar) but a Thread.Sleep is
// much simpler and a good compromise
Thread.Sleep(5);
}
}
}
public static class Log
{
private static string filename;
private static SynchronizedIO<string> writer;
private static StreamWriter file;
public static void Initialize(string filename)
{
if (Log.filename != null)
{
throw new InvalidOperationException("Log already initialized with filename " + Log.filename);
}
if (File.Exists(filename))
{
File.Move(filename, filename + "." + Environment.TickCount.ToString() + Path.GetExtension(filename));
}
File.WriteAllText(filename, "");
Log.filename = filename;
var f = new FileStream(Log.filename, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
Log.file = new StreamWriter(f);
Log.file.AutoFlush = true;
Log.writer = new SynchronizedIO<string>(WriterProc);
}
public static void Write(string message)
{
Log.writer.Enqueue(message);
Console.Write(message);
}
public static void WriteLine(string message)
{
Log.writer.Enqueue(DateTime.Now + "\t" + message + "\r\n");
Console.WriteLine(@message);
}
public static void WriteLine(string message, params object[] args)
{
if (Log.filename == null)
{
throw new InvalidOperationException("Log not initialized.");
}
string s = message;
if (args.Length > 0)
{
s = string.Format(message, args);
}
WriteLine(s);
}
public static void Flush()
{
Log.writer.Flush();
Log.writer.Dispose();
Log.file.Close();
}
private static void WriterProc(string data)
{
file.Write(data);
}
public static void StoreSnapshotAs(string snapshotFilename)
{
Log.writer.Flush();
string text = "";
using (var f = new FileStream(Log.filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
using (var sr = new StreamReader(f))
{
text = sr.ReadToEnd();
}
File.WriteAllText(snapshotFilename, text);
}
}
}