TextFileLogWriter.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. using Wayne.Lib.IO;
  6. namespace Wayne.Lib.Log
  7. {
  8. /// <summary>
  9. /// The textfile log writer is a log writer that has its output in a text log file.
  10. /// </summary>
  11. internal class TextFileLogWriter : LogWriter
  12. {
  13. #region Fields
  14. private bool disposed;
  15. private readonly Dictionary<string, Stream> logFileDict = new Dictionary<string, Stream>();
  16. private readonly object logFileDictLock = new object();
  17. private readonly Dictionary<string, int> currentFileCircleIndex = new Dictionary<string, int>();
  18. private readonly LogConfigTextFilePath logPath;
  19. private readonly LogConfigTextFileMaxSize maxSize;
  20. private readonly LogTextWritingParameters writingParameters;
  21. private readonly IFileSupport fileSupport;
  22. #endregion
  23. #region Construction
  24. public TextFileLogWriter(string logName, LogConfigTextFileOutput output)
  25. : base(logName)
  26. {
  27. logPath = output.LogConfigTextFilePath;
  28. maxSize = output.MaxSize;
  29. writingParameters = new LogTextWritingParameters(output.DateTimeFormat, output.EntityLogKind, output.SuppressCategory, output.KeepFileOpened);
  30. fileSupport = FileSupport.fileSupport;//TODO: Inject this instance.
  31. }
  32. #endregion
  33. #region IDisposable Members
  34. /// <summary>
  35. /// Internal dispose method.
  36. /// </summary>
  37. /// <param name="disposing"></param>
  38. /// <param name="reason"></param>
  39. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  40. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  41. protected override void Dispose(bool disposing, string reason)
  42. {
  43. if (!disposed)
  44. {
  45. try
  46. {
  47. CloseAllFiles(reason);
  48. }
  49. catch (Exception) { }
  50. disposed = true;
  51. }
  52. }
  53. #endregion
  54. #region Public Properties
  55. /// <summary>
  56. /// File path of the log.
  57. /// </summary>
  58. public LogConfigTextFilePath LogConfigTextFilePath
  59. {
  60. get { return logPath; }
  61. }
  62. #endregion
  63. #region Public Methods
  64. /// <summary>
  65. /// Checks so the file stream is open and writes the specified log entry to the log.
  66. /// </summary>
  67. /// <param name="logEntry"></param>
  68. public override void PerformWrite(LogEntry logEntry)
  69. {
  70. if (disposed)
  71. return;
  72. string text = LogTextWriting.GetLogEntryText(this, logEntry, writingParameters);
  73. DebugMarkerLogEntry debugMarkerLogEntry = logEntry as DebugMarkerLogEntry;
  74. if (debugMarkerLogEntry != null)
  75. {
  76. lock (logFileDictLock)
  77. {
  78. string[] fileNames = new string[logFileDict.Keys.Count];
  79. logFileDict.Keys.CopyTo(fileNames, 0); // Use copy of list since dict may change in WriteTextLine!
  80. foreach (string fileName in fileNames)
  81. WriteTextLine(fileName, text, true);
  82. }
  83. }
  84. else
  85. {
  86. string fileName = CheckStream(logEntry.EntityCategory);
  87. WriteTextLine(fileName, text, true);
  88. }
  89. if (!writingParameters.KeepFileOpened)
  90. CloseAllFiles("Should be kept closed...");
  91. }
  92. internal override void PerformListCleaning(DateTime oldestAllowedTouch)
  93. {
  94. base.PerformListCleaning(oldestAllowedTouch);
  95. logPath.PerformListCleaning(oldestAllowedTouch);
  96. }
  97. #endregion
  98. #region Private Methods: Streams and files
  99. /// <summary>
  100. /// Checks if so the file stream is open and that it has the correct date suffix in the filename.
  101. /// </summary>
  102. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  103. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  104. private string CheckStream(EntityCategory entityCategory)
  105. {
  106. string fileName = logPath.ToString(entityCategory);
  107. if (logPath.IsTimeForNewFileName())
  108. {
  109. // Close all log files.
  110. CloseAllFiles("Creating new log file.");
  111. // Get the new file name.
  112. fileName = logPath.ToString(entityCategory);
  113. }
  114. lock (logFileDictLock)
  115. {
  116. if (!logFileDict.ContainsKey(fileName))
  117. {
  118. try
  119. {
  120. // Check so the folder exists.
  121. string parsedFileName = Paths.Parse(fileName);
  122. string directoryName = Path.GetDirectoryName(parsedFileName);
  123. FileSupport.EnsureDirectoryExists(directoryName);
  124. string realFileName = parsedFileName;
  125. // Get real file name if we are using a file circle.
  126. if (maxSize.MaxFileCircleCount > 1)
  127. realFileName = GetNextCircularFileName(parsedFileName);
  128. Stream fileStream = FileSupport.Open(realFileName, FileMode.Append, FileAccess.Write, FileShare.Read, 100, 20);
  129. if (fileStream != null)
  130. {
  131. if (fileStream.Position > maxSize.MaxSizePerFile) //File size checking.
  132. {
  133. fileStream.Close();
  134. fileStream = null;
  135. if (maxSize.MaxFileCircleCount > 1) //If we are using circular logfiles, try to open the next file. This should not be appended, but overwritten.
  136. fileStream = OpenNextCircularFile(parsedFileName);
  137. }
  138. logFileDict[fileName] = fileStream;
  139. }
  140. }
  141. catch (Exception exception)
  142. {
  143. //Logger.FireOnThreadException(new LogException(LogExceptionType.GeneralThreadException,
  144. // 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),
  145. // exception));
  146. }
  147. WriteFileStart(fileName);
  148. }
  149. }
  150. return fileName;
  151. }
  152. /// <summary>
  153. /// Closes all the log files.
  154. /// </summary>
  155. private void CloseAllFiles(string reason)
  156. {
  157. lock (logFileDictLock)
  158. {
  159. foreach (KeyValuePair<string, Stream> pair in logFileDict)
  160. {
  161. WriteFileEnd(pair.Key, reason);
  162. if (pair.Value != null)
  163. pair.Value.Close();
  164. }
  165. logFileDict.Clear();
  166. currentFileCircleIndex.Clear();
  167. }
  168. }
  169. #endregion
  170. #region Private methods: Circular file buffering methods
  171. /// <summary>
  172. /// Opens a circular file for the specified file name. ( Adds an appropriate sequence number before the last extension )
  173. /// </summary>
  174. /// <param name="fileName"></param>
  175. /// <returns></returns>
  176. private Stream OpenNextCircularFile(string fileName)
  177. {
  178. return FileSupport.Open(GetNextCircularFileName(fileName), FileMode.Create, FileAccess.Write, FileShare.Read, 100, 20);
  179. }
  180. /// <summary>
  181. /// Creates the file name for the next circular file.
  182. /// </summary>
  183. /// <param name="fileName"></param>
  184. /// <returns></returns>
  185. private string GetNextCircularFileName(string fileName)
  186. {
  187. int seq = 1;
  188. //Parts of the file name that should be used to recompile the file name, but including a sequence number.
  189. string dir = Path.GetDirectoryName(fileName);
  190. string fileNameStart = Path.GetFileNameWithoutExtension(fileName);
  191. string fileNameEnd = Path.GetExtension(fileName);
  192. //If we already have a sequence number for this file
  193. if (currentFileCircleIndex.TryGetValue(fileName, out seq))
  194. {
  195. //Increase it
  196. seq++;
  197. //Wrap around if reached max.
  198. if (seq > maxSize.MaxFileCircleCount)
  199. seq = 1;
  200. }
  201. else
  202. {
  203. //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.
  204. string searchPath = fileNameStart + "*" + fileNameEnd;
  205. List<string> existingFiles = new List<string>(fileSupport.GetFiles(Path.GetDirectoryName(fileName), searchPath));
  206. if (existingFiles.Count > 0)
  207. {
  208. //Sort the files, to get them in backwards last-written order
  209. existingFiles.Sort(new Comparison<string>(CompareFileInfos));
  210. int lastSeq = 0;
  211. //We must now get the sequence number of the newest file that has one.
  212. for (int i = 0; i < existingFiles.Count; i++)
  213. {
  214. if (TryGetSequenceNumberFromFileName(existingFiles[i], ref lastSeq))
  215. {
  216. seq = lastSeq;
  217. break;
  218. }
  219. }
  220. }
  221. else
  222. {
  223. seq = 1;
  224. }
  225. }
  226. currentFileCircleIndex[fileName] = seq;
  227. return Path.Combine(dir, string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}{2}", fileNameStart, seq, fileNameEnd));
  228. }
  229. /// <summary>
  230. /// Parses a file name and tries to find the circular file sequence number from that.
  231. /// </summary>
  232. /// <param name="lastWrittenFileInfo"></param>
  233. /// <param name="sequenceNumber"></param>
  234. /// <returns></returns>
  235. private static bool TryGetSequenceNumberFromFileName(string lastWrittenFileInfo, ref int sequenceNumber)
  236. {
  237. string[] splitFileName = Path.GetFileName(lastWrittenFileInfo).Split('.');
  238. for (int i = splitFileName.Length - 1; i >= 0; i--)
  239. {
  240. if (char.IsNumber(splitFileName[i], 0))
  241. {
  242. try
  243. {
  244. sequenceNumber = int.Parse(splitFileName[i], System.Globalization.CultureInfo.InvariantCulture);
  245. return true;
  246. }
  247. catch (FormatException) { }
  248. catch (OverflowException) { }
  249. }
  250. }
  251. sequenceNumber = 0;
  252. return false;
  253. }
  254. /// <summary>
  255. /// Compare the file infos, in reverse last write time order.
  256. /// </summary>
  257. /// <param name="f1"></param>
  258. /// <param name="f2"></param>
  259. /// <returns></returns>
  260. int CompareFileInfos(string f1, string f2)
  261. {
  262. return fileSupport.GetLastWriteTime(f2).CompareTo(fileSupport.GetLastWriteTime(f1));
  263. }
  264. #endregion
  265. #region Private Methods: Writing
  266. /// <summary>
  267. /// Writes a message to the log that the file has been appended
  268. /// </summary>
  269. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileStart(System.String)")]
  270. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  271. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  272. private void WriteFileStart(string fileName)
  273. {
  274. if (writingParameters.KeepFileOpened)
  275. {
  276. try
  277. {
  278. DateTime dateTime = DateTime.Now;
  279. string dateTimeText;
  280. if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
  281. dateTimeText = string.Empty;
  282. //else
  283. // dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
  284. //WriteTextLine(fileName, string.Concat(
  285. // dateTimeText + "----------------------------------------------------\r\n",
  286. // 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)),
  287. // dateTimeText + "----------------------------------------------------\r\n"), false);
  288. }
  289. catch (Exception)
  290. {
  291. }
  292. }
  293. }
  294. /// <summary>
  295. /// Writes a message to the log that the file has been closed.
  296. /// </summary>
  297. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileEnd(System.String)")]
  298. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  299. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  300. private void WriteFileEnd(string fileName, string reason)
  301. {
  302. if (writingParameters.KeepFileOpened)
  303. {
  304. try
  305. {
  306. DateTime dateTime = DateTime.Now;
  307. string dateTimeText;
  308. if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
  309. dateTimeText = string.Empty;
  310. //else
  311. // dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
  312. //WriteTextLine(fileName, string.Concat(
  313. // dateTimeText + "----------------------------------------------------\r\n",
  314. // 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)),
  315. // dateTimeText + "----------------------------------------------------\r\n"), false);
  316. }
  317. catch (Exception)
  318. {
  319. }
  320. }
  321. }
  322. /// <summary>
  323. /// Writes the specified text line to the log.
  324. /// </summary>
  325. /// <param name="fileName"></param>
  326. /// <param name="text"></param>
  327. /// <param name="checkFileSize">Dont check file size for internal loggings, like close file etc.</param>
  328. private void WriteTextLine(string fileName, string text, bool checkFileSize)
  329. {
  330. lock (logFileDictLock)
  331. {
  332. Stream fileStream;
  333. if (logFileDict.TryGetValue(fileName, out fileStream) && (fileStream != null) && fileStream.CanWrite)
  334. {
  335. byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(text);
  336. fileStream.Write(textBytes, 0, textBytes.Length);
  337. fileStream.Flush();
  338. if (checkFileSize && (fileStream.Position > maxSize.MaxSizePerFile))
  339. {
  340. WriteFileEnd(fileName, "Max size of file (" + maxSize.MaxSizePerFile.ToString(System.Globalization.CultureInfo.InvariantCulture) + ") reached");
  341. fileStream.Close();
  342. logFileDict[fileName] = null;
  343. //If we are working with circular file buffer, whe should just open the next file.
  344. if (maxSize.MaxFileCircleCount > 1)
  345. {
  346. string parsedFileName = Paths.Parse(fileName);
  347. logFileDict[fileName] = OpenNextCircularFile(parsedFileName);
  348. }
  349. }
  350. }
  351. }
  352. }
  353. #endregion
  354. }
  355. }