using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Wayne.Lib.IO;
namespace Wayne.Lib.Log
{
///
/// The textfile log writer is a log writer that has its output in a text log file.
///
internal class TextFileLogWriter : LogWriter
{
#region Fields
private bool disposed;
private readonly Dictionary logFileDict = new Dictionary();
private readonly object logFileDictLock = new object();
private readonly Dictionary currentFileCircleIndex = new Dictionary();
private readonly LogConfigTextFilePath logPath;
private readonly LogConfigTextFileMaxSize maxSize;
private readonly LogTextWritingParameters writingParameters;
private readonly IFileSupport fileSupport;
#endregion
#region Construction
public TextFileLogWriter(string logName, LogConfigTextFileOutput output)
: base(logName)
{
logPath = output.LogConfigTextFilePath;
maxSize = output.MaxSize;
writingParameters = new LogTextWritingParameters(output.DateTimeFormat, output.EntityLogKind, output.SuppressCategory, output.KeepFileOpened);
fileSupport = FileSupport.fileSupport;//TODO: Inject this instance.
}
#endregion
#region IDisposable Members
///
/// Internal dispose method.
///
///
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
protected override void Dispose(bool disposing, string reason)
{
if (!disposed)
{
try
{
CloseAllFiles(reason);
}
catch (Exception) { }
disposed = true;
}
}
#endregion
#region Public Properties
///
/// File path of the log.
///
public LogConfigTextFilePath LogConfigTextFilePath
{
get { return logPath; }
}
#endregion
#region Public Methods
///
/// Checks so the file stream is open and writes the specified log entry to the log.
///
///
public override void PerformWrite(LogEntry logEntry)
{
if (disposed)
return;
string text = LogTextWriting.GetLogEntryText(this, logEntry, writingParameters);
DebugMarkerLogEntry debugMarkerLogEntry = logEntry as DebugMarkerLogEntry;
if (debugMarkerLogEntry != null)
{
lock (logFileDictLock)
{
string[] fileNames = new string[logFileDict.Keys.Count];
logFileDict.Keys.CopyTo(fileNames, 0); // Use copy of list since dict may change in WriteTextLine!
foreach (string fileName in fileNames)
WriteTextLine(fileName, text, true);
}
}
else
{
string fileName = CheckStream(logEntry.EntityCategory);
WriteTextLine(fileName, text, true);
}
if (!writingParameters.KeepFileOpened)
CloseAllFiles("Should be kept closed...");
}
internal override void PerformListCleaning(DateTime oldestAllowedTouch)
{
base.PerformListCleaning(oldestAllowedTouch);
logPath.PerformListCleaning(oldestAllowedTouch);
}
#endregion
#region Private Methods: Streams and files
///
/// Checks if so the file stream is open and that it has the correct date suffix in the filename.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
private string CheckStream(EntityCategory entityCategory)
{
string fileName = logPath.ToString(entityCategory);
if (logPath.IsTimeForNewFileName())
{
// Close all log files.
CloseAllFiles("Creating new log file.");
// Get the new file name.
fileName = logPath.ToString(entityCategory);
}
lock (logFileDictLock)
{
if (!logFileDict.ContainsKey(fileName))
{
try
{
// Check so the folder exists.
string parsedFileName = Paths.Parse(fileName);
string directoryName = Path.GetDirectoryName(parsedFileName);
FileSupport.EnsureDirectoryExists(directoryName);
string realFileName = parsedFileName;
// Get real file name if we are using a file circle.
if (maxSize.MaxFileCircleCount > 1)
realFileName = GetNextCircularFileName(parsedFileName);
Stream fileStream = FileSupport.Open(realFileName, FileMode.Append, FileAccess.Write, FileShare.Read, 100, 20);
if (fileStream != null)
{
if (fileStream.Position > maxSize.MaxSizePerFile) //File size checking.
{
fileStream.Close();
fileStream = null;
if (maxSize.MaxFileCircleCount > 1) //If we are using circular logfiles, try to open the next file. This should not be appended, but overwritten.
fileStream = OpenNextCircularFile(parsedFileName);
}
logFileDict[fileName] = fileStream;
}
}
catch (Exception exception)
{
//Logger.FireOnThreadException(new LogException(LogExceptionType.GeneralThreadException,
// string.Format(System.Globalization.CultureInfo.InvariantCulture, "An Exception is caught in the Logger thread when trying to open the log file \"{0}\": {1}", fileName, exception.Message),
// exception));
}
WriteFileStart(fileName);
}
}
return fileName;
}
///
/// Closes all the log files.
///
private void CloseAllFiles(string reason)
{
lock (logFileDictLock)
{
foreach (KeyValuePair pair in logFileDict)
{
WriteFileEnd(pair.Key, reason);
if (pair.Value != null)
pair.Value.Close();
}
logFileDict.Clear();
currentFileCircleIndex.Clear();
}
}
#endregion
#region Private methods: Circular file buffering methods
///
/// Opens a circular file for the specified file name. ( Adds an appropriate sequence number before the last extension )
///
///
///
private Stream OpenNextCircularFile(string fileName)
{
return FileSupport.Open(GetNextCircularFileName(fileName), FileMode.Create, FileAccess.Write, FileShare.Read, 100, 20);
}
///
/// Creates the file name for the next circular file.
///
///
///
private string GetNextCircularFileName(string fileName)
{
int seq = 1;
//Parts of the file name that should be used to recompile the file name, but including a sequence number.
string dir = Path.GetDirectoryName(fileName);
string fileNameStart = Path.GetFileNameWithoutExtension(fileName);
string fileNameEnd = Path.GetExtension(fileName);
//If we already have a sequence number for this file
if (currentFileCircleIndex.TryGetValue(fileName, out seq))
{
//Increase it
seq++;
//Wrap around if reached max.
if (seq > maxSize.MaxFileCircleCount)
seq = 1;
}
else
{
//First time for this file name since program start. We see if there are any files on the hard disk and pick the sequence number of the latest written.
string searchPath = fileNameStart + "*" + fileNameEnd;
List existingFiles = new List(fileSupport.GetFiles(Path.GetDirectoryName(fileName), searchPath));
if (existingFiles.Count > 0)
{
//Sort the files, to get them in backwards last-written order
existingFiles.Sort(new Comparison(CompareFileInfos));
int lastSeq = 0;
//We must now get the sequence number of the newest file that has one.
for (int i = 0; i < existingFiles.Count; i++)
{
if (TryGetSequenceNumberFromFileName(existingFiles[i], ref lastSeq))
{
seq = lastSeq;
break;
}
}
}
else
{
seq = 1;
}
}
currentFileCircleIndex[fileName] = seq;
return Path.Combine(dir, string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}{2}", fileNameStart, seq, fileNameEnd));
}
///
/// Parses a file name and tries to find the circular file sequence number from that.
///
///
///
///
private static bool TryGetSequenceNumberFromFileName(string lastWrittenFileInfo, ref int sequenceNumber)
{
string[] splitFileName = Path.GetFileName(lastWrittenFileInfo).Split('.');
for (int i = splitFileName.Length - 1; i >= 0; i--)
{
if (char.IsNumber(splitFileName[i], 0))
{
try
{
sequenceNumber = int.Parse(splitFileName[i], System.Globalization.CultureInfo.InvariantCulture);
return true;
}
catch (FormatException) { }
catch (OverflowException) { }
}
}
sequenceNumber = 0;
return false;
}
///
/// Compare the file infos, in reverse last write time order.
///
///
///
///
int CompareFileInfos(string f1, string f2)
{
return fileSupport.GetLastWriteTime(f2).CompareTo(fileSupport.GetLastWriteTime(f1));
}
#endregion
#region Private Methods: Writing
///
/// Writes a message to the log that the file has been appended
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileStart(System.String)")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
private void WriteFileStart(string fileName)
{
if (writingParameters.KeepFileOpened)
{
try
{
DateTime dateTime = DateTime.Now;
string dateTimeText;
if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
dateTimeText = string.Empty;
//else
// dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
//WriteTextLine(fileName, string.Concat(
// dateTimeText + "----------------------------------------------------\r\n",
// dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "File Appended {0}.\r\n", dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
// dateTimeText + "----------------------------------------------------\r\n"), false);
}
catch (Exception)
{
}
}
}
///
/// Writes a message to the log that the file has been closed.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileEnd(System.String)")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
private void WriteFileEnd(string fileName, string reason)
{
if (writingParameters.KeepFileOpened)
{
try
{
DateTime dateTime = DateTime.Now;
string dateTimeText;
if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
dateTimeText = string.Empty;
//else
// dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
//WriteTextLine(fileName, string.Concat(
// dateTimeText + "----------------------------------------------------\r\n",
// dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "File Closed. Reason: {0} {1}.\r\n", reason, dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
// dateTimeText + "----------------------------------------------------\r\n"), false);
}
catch (Exception)
{
}
}
}
///
/// Writes the specified text line to the log.
///
///
///
/// Dont check file size for internal loggings, like close file etc.
private void WriteTextLine(string fileName, string text, bool checkFileSize)
{
lock (logFileDictLock)
{
Stream fileStream;
if (logFileDict.TryGetValue(fileName, out fileStream) && (fileStream != null) && fileStream.CanWrite)
{
byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(text);
fileStream.Write(textBytes, 0, textBytes.Length);
fileStream.Flush();
if (checkFileSize && (fileStream.Position > maxSize.MaxSizePerFile))
{
WriteFileEnd(fileName, "Max size of file (" + maxSize.MaxSizePerFile.ToString(System.Globalization.CultureInfo.InvariantCulture) + ") reached");
fileStream.Close();
logFileDict[fileName] = null;
//If we are working with circular file buffer, whe should just open the next file.
if (maxSize.MaxFileCircleCount > 1)
{
string parsedFileName = Paths.Parse(fileName);
logFileDict[fileName] = OpenNextCircularFile(parsedFileName);
}
}
}
}
}
#endregion
}
}