using System; using System.Collections.Generic; using System.Text; using System.IO; using Wayne.Lib.IO; namespace Wayne.Lib.Log { /// <summary> /// The textfile log writer is a log writer that has its output in a text log file. /// </summary> internal class TextFileLogWriter : LogWriter { #region Fields private bool disposed; private readonly Dictionary<string, Stream> logFileDict = new Dictionary<string, Stream>(); private readonly object logFileDictLock = new object(); private readonly Dictionary<string, int> currentFileCircleIndex = new Dictionary<string, int>(); private readonly LogConfigTextFilePath logPath; private readonly LogConfigTextFileMaxSize maxSize; private readonly LogTextWritingParameters writingParameters; private Dictionary<string, string> openedRealFileNames = new Dictionary<string, string>(); private Queue<Dictionary<string, string>> openedFileSets = new Queue<Dictionary<string, string>>(); private bool shortFileMessages; private static Dictionary<string, LogConfigTextFilePath> cleanedLogs = new Dictionary<string, LogConfigTextFilePath>(); #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); } } /// <summary> /// Cleans up files matching a log path once per process instantiation - checks a static dictionary for the path /// </summary> /// <param name="logPath"></param> 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 /// <summary> /// Internal dispose method. /// </summary> /// <param name="disposing"></param> /// <param name="reason"></param> [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 /// <summary> /// File path of the log. /// </summary> public LogConfigTextFilePath LogConfigTextFilePath { get { return logPath; } } #endregion #region Public Methods /// <summary> /// Checks so the file stream is open and writes the specified log entry to the log. /// </summary> /// <param name="logEntry"></param> 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 /// <summary> /// Checks if so the file stream is open and that it has the correct date suffix in the filename. /// </summary> [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<string, string>(); 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; } /// <summary> /// Delete filesets older than our specified limits /// </summary> private void MaybeCleanOutDatedFiles() { if (openedFileSets.Count > maxSize.MaxDays) { Dictionary<string, string> filesToPurge = openedFileSets.Dequeue(); foreach (string f in filesToPurge.Values) { try { File.Delete(f); } catch { } } } } /// <summary> /// Closes all log files /// </summary> /// <param name="reason"></param> /// <returns>list of strings of closed files</returns> private List<string> CloseAllFiles(string reason) { List<String> closedFiles = new List<String>(); lock (logFileDictLock) { foreach (KeyValuePair<string, Stream> 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 /// <summary> /// Opens a circular file for the specified file name. ( Adds an appropriate sequence number before the last extension ) /// </summary> /// <param name="fileName"></param> /// <returns></returns> 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); } /// <summary> /// Creates the file name for the next circular file. /// </summary> /// <param name="fileName"></param> /// <returns></returns> 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<FileInfo> existingFileInfos = new List<FileInfo>(di.GetFiles(searchPath)); if (existingFileInfos.Count > 0) { //Sort the files, to get them in backwards last-written order existingFileInfos.Sort(new Comparison<FileInfo>(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)); } /// <summary> /// Parses a file name and tries to find the circular file sequence number from that. /// </summary> /// <param name="lastWrittenFileInfo"></param> /// <param name="sequenceNumber"></param> /// <returns></returns> 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; } /// <summary> /// Compare the file infos, in reverse last write time order. /// </summary> /// <param name="f1"></param> /// <param name="f2"></param> /// <returns></returns> static int CompareFileInfos(FileInfo f1, FileInfo f2) { return f2.LastWriteTime.CompareTo(f1.LastWriteTime); } #endregion #region Private Methods: Writing /// <summary> /// Writes a message to the log that the file has been appended /// </summary> [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) { } } } /// <summary> /// Writes a message to the log that the file has been closed. /// </summary> [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) { } } } /// <summary> /// Writes the specified text line to the log. /// </summary> /// <param name="fileName"></param> /// <param name="text"></param> /// <param name="checkFileSize">Dont check file size for internal loggings, like close file etc.</param> 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 } }