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 Dictionary openedRealFileNames = new Dictionary();
private Queue> openedFileSets = new Queue>();
private bool shortFileMessages;
private static Dictionary cleanedLogs = new Dictionary();
#endregion
#region Construction
public TextFileLogWriter(string logName, LogConfigTextFileOutput output)
: base(logName)
{
shortFileMessages = output.ShortFileMessages;
logPath = output.LogConfigTextFilePath;
maxSize = output.MaxSize;
writingParameters = new LogTextWritingParameters(output.DateTimeFormat, output.EntityLogKind, output.SuppressCategory, output.KeepFileOpened);
if (maxSize.CleanOnStartup)
{
MaybeClean(logPath);
}
}
///
/// Cleans up files matching a log path once per process instantiation - checks a static dictionary for the path
///
///
protected void MaybeClean(LogConfigTextFilePath logPath)
{
string prefix = logPath.BaseName;
if (cleanedLogs.ContainsKey(prefix)) return;
try
{
string directory = Path.GetDirectoryName(prefix);
string filePrefix = Path.GetFileNameWithoutExtension(prefix);
DirectoryInfo di = new DirectoryInfo(directory);
if (di.Exists)
{
FileInfo[] fiArray = di.GetFiles();
foreach (FileInfo fi in fiArray)
{
if (fi.Name.StartsWith(filePrefix))
{
fi.Delete();
}
}
}
}
catch
{
// ignore all errors
}
}
#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.");
openedFileSets.Enqueue(openedRealFileNames);
openedRealFileNames = new Dictionary();
MaybeCleanOutDatedFiles();
// 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 (!openedRealFileNames.ContainsKey(realFileName))
{
openedRealFileNames.Add(realFileName, realFileName);
}
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;
}
///
/// Delete filesets older than our specified limits
///
private void MaybeCleanOutDatedFiles()
{
if (openedFileSets.Count > maxSize.MaxDays)
{
Dictionary filesToPurge = openedFileSets.Dequeue();
foreach (string f in filesToPurge.Values)
{
try
{
File.Delete(f);
}
catch { }
}
}
}
///
/// Closes all log files
///
///
/// list of strings of closed files
private List CloseAllFiles(string reason)
{
List closedFiles = new List();
lock (logFileDictLock)
{
foreach (KeyValuePair pair in logFileDict)
{
WriteFileEnd(pair.Key, reason);
if (pair.Value != null)
pair.Value.Close();
closedFiles.Add(pair.Key);
}
logFileDict.Clear();
currentFileCircleIndex.Clear();
}
return closedFiles;
}
#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)
{
string realFileName = GetNextCircularFileName(fileName);
if (!openedRealFileNames.ContainsKey(realFileName))
{
openedRealFileNames.Add(realFileName, realFileName);
}
return FileSupport.Open(realFileName, 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;
DirectoryInfo di = new DirectoryInfo(Path.GetDirectoryName(fileName));
List existingFileInfos = new List(di.GetFiles(searchPath));
if (existingFileInfos.Count > 0)
{
//Sort the files, to get them in backwards last-written order
existingFileInfos.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 < existingFileInfos.Count; i++)
{
if (TryGetSequenceNumberFromFileName(existingFileInfos[i], ref lastSeq))
{
seq = lastSeq;
break;
}
}
}
}
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(FileInfo lastWrittenFileInfo, ref int sequenceNumber)
{
string[] splitFileName = lastWrittenFileInfo.Name.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.
///
///
///
///
static int CompareFileInfos(FileInfo f1, FileInfo f2)
{
return f2.LastWriteTime.CompareTo(f1.LastWriteTime);
}
#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) + " ";
if (shortFileMessages)
{
WriteTextLine(fileName,
dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "TextFileLogWriter: FileAppended {0}. ------------------------------------------------\r\n",
dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff",
System.Globalization.CultureInfo.InvariantCulture)),
false);
}
else
{
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) + " ";
if (shortFileMessages)
{
WriteTextLine(fileName,
dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "TextFileLogWriter: FileClosed Reason: {0} {1}. ------------------------------------------------\r\n", reason, dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
false);
}
else
{
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 = 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)
logFileDict[fileName] = OpenNextCircularFile(fileName);
}
}
}
}
#endregion
}
}