#region Apache License // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to you under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #endregion using System; using System.Collections; using System.Globalization; using System.IO; using log4net.Util; using log4net.Core; using System.Threading; namespace log4net.Appender { #if CONFIRM_WIN32_FILE_SHAREMODES // The following sounds good, and I though it was the case, but after // further testing on Windows I have not been able to confirm it. /// On the Windows platform if another process has a write lock on the file /// that is to be deleted, but allows shared read access to the file then the /// file can be moved, but cannot be deleted. If the other process also allows /// shared delete access to the file then the file will be deleted once that /// process closes the file. If it is necessary to open the log file or any /// of the backup files outside of this appender for either read or /// write access please ensure that read and delete share modes are enabled. #endif /// /// Appender that rolls log files based on size or date or both. /// /// /// /// RollingFileAppender can roll log files based on size or date or both /// depending on the setting of the property. /// When set to the log file will be rolled /// once its size exceeds the . /// When set to the log file will be rolled /// once the date boundary specified in the property /// is crossed. /// When set to the log file will be /// rolled once the date boundary specified in the property /// is crossed, but within a date boundary the file will also be rolled /// once its size exceeds the . /// When set to the log file will be rolled when /// the appender is configured. This effectively means that the log file can be /// rolled once per program execution. /// /// /// A of few additional optional features have been added: /// /// Attach date pattern for current log file /// Backup number increments for newer files /// Infinite number of backups by file size /// /// /// /// /// /// For large or infinite numbers of backup files a /// greater than zero is highly recommended, otherwise all the backup files need /// to be renamed each time a new backup is created. /// /// /// When Date/Time based rolling is used setting /// to will reduce the number of file renamings to few or none. /// /// /// /// /// /// Changing or without clearing /// the log file directory of backup files will cause unexpected and unwanted side effects. /// /// /// /// /// If Date/Time based rolling is enabled this appender will attempt to roll existing files /// in the directory without a Date/Time tag based on the last write date of the base log file. /// The appender only rolls the log file when a message is logged. If Date/Time based rolling /// is enabled then the appender will not roll the log file at the Date/Time boundary but /// at the point when the next message is logged after the boundary has been crossed. /// /// /// /// The extends the and /// has the same behavior when opening the log file. /// The appender will first try to open the file for writing when /// is called. This will typically be during configuration. /// If the file cannot be opened for writing the appender will attempt /// to open the file again each time a message is logged to the appender. /// If the file cannot be opened for writing when a message is logged then /// the message will be discarded by this appender. /// /// /// When rolling a backup file necessitates deleting an older backup file the /// file to be deleted is moved to a temporary name before being deleted. /// /// /// /// /// A maximum number of backup files when rolling on date/time boundaries is not supported. /// /// /// /// Nicko Cadell /// Gert Driesen /// Aspi Havewala /// Douglas de la Torre /// Edward Smit public class RollingFileAppender : FileAppender { #region Public Enums /// /// Style of rolling to use /// /// /// /// Style of rolling to use /// /// public enum RollingMode { /// /// Roll files once per program execution /// /// /// /// Roll files once per program execution. /// Well really once each time this appender is /// configured. /// /// /// Setting this option also sets AppendToFile to /// false on the RollingFileAppender, otherwise /// this appender would just be a normal file appender. /// /// Once = 0, /// /// Roll files based only on the size of the file /// Size = 1, /// /// Roll files based only on the date /// Date = 2, /// /// Roll files based on both the size and date of the file /// Composite = 3 } #endregion #region Protected Enums /// /// The code assumes that the following 'time' constants are in a increasing sequence. /// /// /// /// The code assumes that the following 'time' constants are in a increasing sequence. /// /// protected enum RollPoint { /// /// Roll the log not based on the date /// InvalidRollPoint =-1, /// /// Roll the log for each minute /// TopOfMinute = 0, /// /// Roll the log for each hour /// TopOfHour = 1, /// /// Roll the log twice a day (midday and midnight) /// HalfDay = 2, /// /// Roll the log each day (midnight) /// TopOfDay = 3, /// /// Roll the log each week /// TopOfWeek = 4, /// /// Roll the log each month /// TopOfMonth = 5 } #endregion Protected Enums #region Public Instance Constructors /// /// Initializes a new instance of the class. /// /// /// /// Default constructor. /// /// public RollingFileAppender() { } /// /// Cleans up all resources used by this appender. /// ~RollingFileAppender() { #if !NETCF if (m_mutexForRolling != null) { #if NET_4_0 || MONO_4_0 || NETSTANDARD1_3 m_mutexForRolling.Dispose(); #else m_mutexForRolling.Close(); #endif m_mutexForRolling = null; } #endif } #endregion Public Instance Constructors #region Public Instance Properties #if !NET_1_0 && !CLI_1_0 && !NETCF /// /// Gets or sets the strategy for determining the current date and time. The default /// implementation is to use LocalDateTime which internally calls through to DateTime.Now. /// DateTime.UtcNow may be used on frameworks newer than .NET 1.0 by specifying /// . /// /// /// An implementation of the interface which returns the current date and time. /// /// /// /// Gets or sets the used to return the current date and time. /// /// /// There are two built strategies for determining the current date and time, /// /// and . /// /// /// The default strategy is . /// /// #else /// /// Gets or sets the strategy for determining the current date and time. The default /// implementation is to use LocalDateTime which internally calls through to DateTime.Now. /// /// /// An implementation of the interface which returns the current date and time. /// /// /// /// Gets or sets the used to return the current date and time. /// /// /// The default strategy is . /// /// #endif public IDateTime DateTimeStrategy { get { return m_dateTime; } set { m_dateTime = value; } } /// /// Gets or sets the date pattern to be used for generating file names /// when rolling over on date. /// /// /// The date pattern to be used for generating file names when rolling /// over on date. /// /// /// /// Takes a string in the same format as expected by /// . /// /// /// This property determines the rollover schedule when rolling over /// on date. /// /// public string DatePattern { get { return m_datePattern; } set { m_datePattern = value; } } /// /// Gets or sets the maximum number of backup files that are kept before /// the oldest is erased. /// /// /// The maximum number of backup files that are kept before the oldest is /// erased. /// /// /// /// If set to zero, then there will be no backup files and the log file /// will be truncated when it reaches . /// /// /// If a negative number is supplied then no deletions will be made. Note /// that this could result in very slow performance as a large number of /// files are rolled over unless is used. /// /// /// The maximum applies to each time based group of files and /// not the total. /// /// public int MaxSizeRollBackups { get { return m_maxSizeRollBackups; } set { m_maxSizeRollBackups = value; } } /// /// Gets or sets the maximum size that the output file is allowed to reach /// before being rolled over to backup files. /// /// /// The maximum size in bytes that the output file is allowed to reach before being /// rolled over to backup files. /// /// /// /// This property is equivalent to except /// that it is required for differentiating the setter taking a /// argument from the setter taking a /// argument. /// /// /// The default maximum file size is 10MB (10*1024*1024). /// /// public long MaxFileSize { get { return m_maxFileSize; } set { m_maxFileSize = value; } } /// /// Gets or sets the maximum size that the output file is allowed to reach /// before being rolled over to backup files. /// /// /// The maximum size that the output file is allowed to reach before being /// rolled over to backup files. /// /// /// /// This property allows you to specify the maximum size with the /// suffixes "KB", "MB" or "GB" so that the size is interpreted being /// expressed respectively in kilobytes, megabytes or gigabytes. /// /// /// For example, the value "10KB" will be interpreted as 10240 bytes. /// /// /// The default maximum file size is 10MB. /// /// /// If you have the option to set the maximum file size programmatically /// consider using the property instead as this /// allows you to set the size in bytes as a . /// /// public string MaximumFileSize { get { return m_maxFileSize.ToString(NumberFormatInfo.InvariantInfo); } set { m_maxFileSize = OptionConverter.ToFileSize(value, m_maxFileSize + 1); } } /// /// Gets or sets the rolling file count direction. /// /// /// The rolling file count direction. /// /// /// /// Indicates if the current file is the lowest numbered file or the /// highest numbered file. /// /// /// By default newer files have lower numbers ( < 0), /// i.e. log.1 is most recent, log.5 is the 5th backup, etc... /// /// /// >= 0 does the opposite i.e. /// log.1 is the first backup made, log.5 is the 5th backup made, etc. /// For infinite backups use >= 0 to reduce /// rollover costs. /// /// The default file count direction is -1. /// public int CountDirection { get { return m_countDirection; } set { m_countDirection = value; } } /// /// Gets or sets the rolling style. /// /// The rolling style. /// /// /// The default rolling style is . /// /// /// When set to this appender's /// property is set to false, otherwise /// the appender would append to a single file rather than rolling /// the file each time it is opened. /// /// public RollingMode RollingStyle { get { return m_rollingStyle; } set { m_rollingStyle = value; switch (m_rollingStyle) { case RollingMode.Once: m_rollDate = false; m_rollSize = false; this.AppendToFile = false; break; case RollingMode.Size: m_rollDate = false; m_rollSize = true; break; case RollingMode.Date: m_rollDate = true; m_rollSize = false; break; case RollingMode.Composite: m_rollDate = true; m_rollSize = true; break; } } } /// /// Gets or sets a value indicating whether to preserve the file name extension when rolling. /// /// /// true if the file name extension should be preserved. /// /// /// /// By default file.log is rolled to file.log.yyyy-MM-dd or file.log.curSizeRollBackup. /// However, under Windows the new file name will loose any program associations as the /// extension is changed. Optionally file.log can be renamed to file.yyyy-MM-dd.log or /// file.curSizeRollBackup.log to maintain any program associations. /// /// public bool PreserveLogFileNameExtension { get { return m_preserveLogFileNameExtension; } set { m_preserveLogFileNameExtension = value; } } /// /// Gets or sets a value indicating whether to always log to /// the same file. /// /// /// true if always should be logged to the same file, otherwise false. /// /// /// /// By default file.log is always the current file. Optionally /// file.log.yyyy-mm-dd for current formatted datePattern can by the currently /// logging file (or file.log.curSizeRollBackup or even /// file.log.yyyy-mm-dd.curSizeRollBackup). /// /// /// This will make time based rollovers with a large number of backups /// much faster as the appender it won't have to rename all the backups! /// /// public bool StaticLogFileName { get { return m_staticLogFileName; } set { m_staticLogFileName = value; } } #endregion Public Instance Properties #region Private Static Fields /// /// The fully qualified type of the RollingFileAppender class. /// /// /// Used by the internal logger to record the Type of the /// log message. /// private readonly static Type declaringType = typeof(RollingFileAppender); #endregion Private Static Fields #region Override implementation of FileAppender /// /// Sets the quiet writer being used. /// /// /// This method can be overridden by sub classes. /// /// the writer to set override protected void SetQWForFiles(TextWriter writer) { QuietWriter = new CountingQuietTextWriter(writer, ErrorHandler); } /// /// Write out a logging event. /// /// the event to write to file. /// /// /// Handles append time behavior for RollingFileAppender. This checks /// if a roll over either by date (checked first) or time (checked second) /// is need and then appends to the file last. /// /// override protected void Append(LoggingEvent loggingEvent) { AdjustFileBeforeAppend(); base.Append(loggingEvent); } /// /// Write out an array of logging events. /// /// the events to write to file. /// /// /// Handles append time behavior for RollingFileAppender. This checks /// if a roll over either by date (checked first) or time (checked second) /// is need and then appends to the file last. /// /// override protected void Append(LoggingEvent[] loggingEvents) { AdjustFileBeforeAppend(); base.Append(loggingEvents); } /// /// Performs any required rolling before outputting the next event /// /// /// /// Handles append time behavior for RollingFileAppender. This checks /// if a roll over either by date (checked first) or time (checked second) /// is need and then appends to the file last. /// /// virtual protected void AdjustFileBeforeAppend() { // reuse the file appenders locking model to lock the rolling #if !NETCF try { // if rolling should be locked, acquire the lock if (m_mutexForRolling != null) { m_mutexForRolling.WaitOne(); } #endif if (m_rollDate) { DateTime n = m_dateTime.Now; if (n >= m_nextCheck) { m_now = n; m_nextCheck = NextCheckDate(m_now, m_rollPoint); RollOverTime(true); } } if (m_rollSize) { if ((File != null) && ((CountingQuietTextWriter)QuietWriter).Count >= m_maxFileSize) { RollOverSize(); } } #if !NETCF } finally { // if rolling should be locked, release the lock if (m_mutexForRolling != null) { m_mutexForRolling.ReleaseMutex(); } } #endif } /// /// Creates and opens the file for logging. If /// is false then the fully qualified name is determined and used. /// /// the name of the file to open /// true to append to existing file /// /// This method will ensure that the directory structure /// for the specified exists. /// override protected void OpenFile(string fileName, bool append) { lock(this) { fileName = GetNextOutputFileName(fileName); // Calculate the current size of the file long currentCount = 0; if (append) { using(SecurityContext.Impersonate(this)) { if (System.IO.File.Exists(fileName)) { currentCount = (new FileInfo(fileName)).Length; } } } else { if (LogLog.IsErrorEnabled) { // Internal check that the file is not being overwritten // If not Appending to an existing file we should have rolled the file out of the // way. Therefore we should not be over-writing an existing file. // The only exception is if we are not allowed to roll the existing file away. if (m_maxSizeRollBackups != 0 && FileExists(fileName)) { LogLog.Error(declaringType, "RollingFileAppender: INTERNAL ERROR. Append is False but OutputFile ["+fileName+"] already exists."); } } } if (!m_staticLogFileName) { m_scheduledFilename = fileName; } // Open the file (call the base class to do it) base.OpenFile(fileName, append); // Set the file size onto the counting writer ((CountingQuietTextWriter)QuietWriter).Count = currentCount; } } /// /// Get the current output file name /// /// the base file name /// the output file name /// /// The output file name is based on the base fileName specified. /// If is set then the output /// file name is the same as the base file passed in. Otherwise /// the output file depends on the date pattern, on the count /// direction or both. /// protected string GetNextOutputFileName(string fileName) { if (!m_staticLogFileName) { fileName = fileName.Trim(); if (m_rollDate) { fileName = CombinePath(fileName, m_now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo)); } if (m_countDirection >= 0) { fileName = CombinePath(fileName, "." + m_curSizeRollBackups); } } return fileName; } #endregion #region Initialize Options /// /// Determines curSizeRollBackups (only within the current roll point) /// private void DetermineCurSizeRollBackups() { m_curSizeRollBackups = 0; string fullPath = null; string fileName = null; using(SecurityContext.Impersonate(this)) { fullPath = System.IO.Path.GetFullPath(m_baseFileName); fileName = System.IO.Path.GetFileName(fullPath); } ArrayList arrayFiles = GetExistingFiles(fullPath); InitializeRollBackups(fileName, arrayFiles); LogLog.Debug(declaringType, "curSizeRollBackups starts at ["+m_curSizeRollBackups+"]"); } /// /// Generates a wildcard pattern that can be used to find all files /// that are similar to the base file name. /// /// /// private string GetWildcardPatternForFile(string baseFileName) { if (m_preserveLogFileNameExtension) { return Path.GetFileNameWithoutExtension(baseFileName) + "*" + Path.GetExtension(baseFileName); } else { return baseFileName + '*'; } } /// /// Builds a list of filenames for all files matching the base filename plus a file /// pattern. /// /// /// private ArrayList GetExistingFiles(string baseFilePath) { ArrayList alFiles = new ArrayList(); string directory = null; using(SecurityContext.Impersonate(this)) { string fullPath = Path.GetFullPath(baseFilePath); directory = Path.GetDirectoryName(fullPath); if (Directory.Exists(directory)) { string baseFileName = Path.GetFileName(fullPath); string[] files = Directory.GetFiles(directory, GetWildcardPatternForFile(baseFileName)); if (files != null) { for (int i = 0; i < files.Length; i++) { string curFileName = Path.GetFileName(files[i]); if (curFileName.StartsWith(Path.GetFileNameWithoutExtension(baseFileName))) { alFiles.Add(curFileName); } } } } } LogLog.Debug(declaringType, "Searched for existing files in ["+directory+"]"); return alFiles; } /// /// Initiates a roll over if needed for crossing a date boundary since the last run. /// private void RollOverIfDateBoundaryCrossing() { if (m_staticLogFileName && m_rollDate) { if (FileExists(m_baseFileName)) { DateTime last; using(SecurityContext.Impersonate(this)) { #if !NET_1_0 && !CLI_1_0 && !NETCF if (DateTimeStrategy is UniversalDateTime) { last = System.IO.File.GetLastWriteTimeUtc(m_baseFileName); } else { #endif last = System.IO.File.GetLastWriteTime(m_baseFileName); #if !NET_1_0 && !CLI_1_0 && !NETCF } #endif } LogLog.Debug(declaringType, "["+last.ToString(m_datePattern,System.Globalization.DateTimeFormatInfo.InvariantInfo)+"] vs. ["+m_now.ToString(m_datePattern,System.Globalization.DateTimeFormatInfo.InvariantInfo)+"]"); if (!(last.ToString(m_datePattern,System.Globalization.DateTimeFormatInfo.InvariantInfo).Equals(m_now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo)))) { m_scheduledFilename = CombinePath(m_baseFileName, last.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo)); LogLog.Debug(declaringType, "Initial roll over to ["+m_scheduledFilename+"]"); RollOverTime(false); LogLog.Debug(declaringType, "curSizeRollBackups after rollOver at ["+m_curSizeRollBackups+"]"); } } } } /// /// Initializes based on existing conditions at time of . /// /// /// /// Initializes based on existing conditions at time of . /// The following is done /// /// determine curSizeRollBackups (only within the current roll point) /// initiates a roll over if needed for crossing a date boundary since the last run. /// /// /// protected void ExistingInit() { DetermineCurSizeRollBackups(); RollOverIfDateBoundaryCrossing(); // If file exists and we are not appending then roll it out of the way if (AppendToFile == false) { bool fileExists = false; string fileName = GetNextOutputFileName(m_baseFileName); using(SecurityContext.Impersonate(this)) { fileExists = System.IO.File.Exists(fileName); } if (fileExists) { if (m_maxSizeRollBackups == 0) { LogLog.Debug(declaringType, "Output file ["+fileName+"] already exists. MaxSizeRollBackups is 0; cannot roll. Overwriting existing file."); } else { LogLog.Debug(declaringType, "Output file ["+fileName+"] already exists. Not appending to file. Rolling existing file out of the way."); RollOverRenameFiles(fileName); } } } } /// /// Does the work of bumping the 'current' file counter higher /// to the highest count when an incremental file name is seen. /// The highest count is either the first file (when count direction /// is greater than 0) or the last file (when count direction less than 0). /// In either case, we want to know the highest count that is present. /// /// /// private void InitializeFromOneFile(string baseFile, string curFileName) { if (curFileName.StartsWith(Path.GetFileNameWithoutExtension(baseFile)) == false) { // This is not a log file, so ignore return; } if (curFileName.Equals(baseFile)) { // Base log file is not an incremented logfile (.1 or .2, etc) return; } /* if (m_staticLogFileName) { int endLength = curFileName.Length - index; if (baseFile.Length + endLength != curFileName.Length) { // file is probably scheduledFilename + .x so I don't care return; } } */ // Only look for files in the current roll point if (m_rollDate && !m_staticLogFileName) { string date = m_dateTime.Now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo); string prefix = m_preserveLogFileNameExtension ? Path.GetFileNameWithoutExtension(baseFile) + date : baseFile + date; string suffix = m_preserveLogFileNameExtension ? Path.GetExtension(baseFile) : ""; if (!curFileName.StartsWith(prefix) || !curFileName.EndsWith(suffix)) { LogLog.Debug(declaringType, "Ignoring file ["+curFileName+"] because it is from a different date period"); return; } } try { // Bump the counter up to the highest count seen so far int backup = GetBackUpIndex(curFileName); // caution: we might get a false positive when certain // date patterns such as yyyyMMdd are used...those are // valid number but aren't the kind of back up index // we're looking for if (backup > m_curSizeRollBackups) { if (0 == m_maxSizeRollBackups) { // Stay at zero when zero backups are desired } else if (-1 == m_maxSizeRollBackups) { // Infinite backups, so go as high as the highest value m_curSizeRollBackups = backup; } else { // Backups limited to a finite number if (m_countDirection >= 0) { // Go with the highest file when counting up m_curSizeRollBackups = backup; } else { // Clip to the limit when counting down if (backup <= m_maxSizeRollBackups) { m_curSizeRollBackups = backup; } } } LogLog.Debug(declaringType, "File name [" + curFileName + "] moves current count to [" + m_curSizeRollBackups + "]"); } } catch(FormatException) { //this happens when file.log -> file.log.yyyy-MM-dd which is normal //when staticLogFileName == false LogLog.Debug(declaringType, "Encountered a backup file not ending in .x ["+curFileName+"]"); } } /// /// Attempts to extract a number from the end of the file name that indicates /// the number of the times the file has been rolled over. /// /// /// Certain date pattern extensions like yyyyMMdd will be parsed as valid backup indexes. /// /// /// private int GetBackUpIndex(string curFileName) { int backUpIndex = -1; string fileName = curFileName; if (m_preserveLogFileNameExtension) { fileName = Path.GetFileNameWithoutExtension(fileName); } int index = fileName.LastIndexOf("."); if (index > 0) { // if the "yyyy-MM-dd" component of file.log.yyyy-MM-dd is passed to TryParse // it will gracefully fail and return backUpIndex will be 0 SystemInfo.TryParse(fileName.Substring(index + 1), out backUpIndex); } return backUpIndex; } /// /// Takes a list of files and a base file name, and looks for /// 'incremented' versions of the base file. Bumps the max /// count up to the highest count seen. /// /// /// private void InitializeRollBackups(string baseFile, ArrayList arrayFiles) { if (null != arrayFiles) { string baseFileLower = baseFile.ToLower(System.Globalization.CultureInfo.InvariantCulture); foreach(string curFileName in arrayFiles) { InitializeFromOneFile(baseFileLower, curFileName.ToLower(System.Globalization.CultureInfo.InvariantCulture)); } } } /// /// Calculates the RollPoint for the datePattern supplied. /// /// the date pattern to calculate the check period for /// The RollPoint that is most accurate for the date pattern supplied /// /// Essentially the date pattern is examined to determine what the /// most suitable roll point is. The roll point chosen is the roll point /// with the smallest period that can be detected using the date pattern /// supplied. i.e. if the date pattern only outputs the year, month, day /// and hour then the smallest roll point that can be detected would be /// and hourly roll point as minutes could not be detected. /// private RollPoint ComputeCheckPeriod(string datePattern) { // s_date1970 is 1970-01-01 00:00:00 this is UniversalSortableDateTimePattern // (based on ISO 8601) using universal time. This date is used for reference // purposes to calculate the resolution of the date pattern. // Get string representation of base line date string r0 = s_date1970.ToString(datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo); // Check each type of rolling mode starting with the smallest increment. for(int i = (int)RollPoint.TopOfMinute; i <= (int)RollPoint.TopOfMonth; i++) { // Get string representation of next pattern string r1 = NextCheckDate(s_date1970, (RollPoint)i).ToString(datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo); LogLog.Debug(declaringType, "Type = ["+i+"], r0 = ["+r0+"], r1 = ["+r1+"]"); // Check if the string representations are different if (r0 != null && r1 != null && !r0.Equals(r1)) { // Found highest precision roll point return (RollPoint)i; } } return RollPoint.InvalidRollPoint; // Deliberately head for trouble... } /// /// Initialize the appender based on the options set /// /// /// /// This is part of the delayed object /// activation scheme. The method must /// be called on this object after the configuration properties have /// been set. Until is called this /// object is in an undefined state and must not be used. /// /// /// If any of the configuration properties are modified then /// must be called again. /// /// /// Sets initial conditions including date/time roll over information, first check, /// scheduledFilename, and calls to initialize /// the current number of backups. /// /// override public void ActivateOptions() { if (m_dateTime == null) { m_dateTime = new LocalDateTime(); } if (m_rollDate && m_datePattern != null) { m_now = m_dateTime.Now; m_rollPoint = ComputeCheckPeriod(m_datePattern); if (m_rollPoint == RollPoint.InvalidRollPoint) { throw new ArgumentException("Invalid RollPoint, unable to parse ["+m_datePattern+"]"); } // next line added as this removes the name check in rollOver m_nextCheck = NextCheckDate(m_now, m_rollPoint); } else { if (m_rollDate) { ErrorHandler.Error("Either DatePattern or rollingStyle options are not set for ["+Name+"]."); } } if (SecurityContext == null) { SecurityContext = SecurityContextProvider.DefaultProvider.CreateSecurityContext(this); } using(SecurityContext.Impersonate(this)) { // Must convert the FileAppender's m_filePath to an absolute path before we // call ExistingInit(). This will be done by the base.ActivateOptions() but // we need to duplicate that functionality here first. base.File = ConvertToFullPath(base.File.Trim()); // Store fully qualified base file name m_baseFileName = base.File; } #if !NETCF // initialize the mutex that is used to lock rolling m_mutexForRolling = new Mutex(false, m_baseFileName.Replace("\\", "_").Replace(":", "_").Replace("/", "_")); #endif if (m_rollDate && File != null && m_scheduledFilename == null) { m_scheduledFilename = CombinePath(File, m_now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo)); } ExistingInit(); base.ActivateOptions(); } #endregion #region Roll File /// /// /// /// /// .1, .2, .3, etc. /// private string CombinePath(string path1, string path2) { string extension = Path.GetExtension(path1); if (m_preserveLogFileNameExtension && extension.Length > 0) { return Path.Combine(Path.GetDirectoryName(path1), Path.GetFileNameWithoutExtension(path1) + path2 + extension); } else { return path1 + path2; } } /// /// Rollover the file(s) to date/time tagged file(s). /// /// set to true if the file to be rolled is currently open /// /// /// Rollover the file(s) to date/time tagged file(s). /// Resets curSizeRollBackups. /// If fileIsOpen is set then the new file is opened (through SafeOpenFile). /// /// protected void RollOverTime(bool fileIsOpen) { if (m_staticLogFileName) { // Compute filename, but only if datePattern is specified if (m_datePattern == null) { ErrorHandler.Error("Missing DatePattern option in rollOver()."); return; } //is the new file name equivalent to the 'current' one //something has gone wrong if we hit this -- we should only //roll over if the new file will be different from the old string dateFormat = m_now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo); if (m_scheduledFilename.Equals(CombinePath(File, dateFormat))) { ErrorHandler.Error("Compare " + m_scheduledFilename + " : " + CombinePath(File, dateFormat)); return; } if (fileIsOpen) { // close current file, and rename it to datedFilename this.CloseFile(); } //we may have to roll over a large number of backups here for (int i = 1; i <= m_curSizeRollBackups; i++) { string from = CombinePath(File, "." + i); string to = CombinePath(m_scheduledFilename, "." + i); RollFile(from, to); } RollFile(File, m_scheduledFilename); } //We've cleared out the old date and are ready for the new m_curSizeRollBackups = 0; //new scheduled name m_scheduledFilename = CombinePath(File, m_now.ToString(m_datePattern, System.Globalization.DateTimeFormatInfo.InvariantInfo)); if (fileIsOpen) { // This will also close the file. This is OK since multiple close operations are safe. SafeOpenFile(m_baseFileName, false); } } /// /// Renames file to file . /// /// Name of existing file to roll. /// New name for file. /// /// /// Renames file to file . It /// also checks for existence of target file and deletes if it does. /// /// protected void RollFile(string fromFile, string toFile) { if (FileExists(fromFile)) { // Delete the toFile if it exists DeleteFile(toFile); // We may not have permission to move the file, or the file may be locked try { LogLog.Debug(declaringType, "Moving [" + fromFile + "] -> [" + toFile + "]"); using(SecurityContext.Impersonate(this)) { System.IO.File.Move(fromFile, toFile); } } catch(Exception moveEx) { ErrorHandler.Error("Exception while rolling file [" + fromFile + "] -> [" + toFile + "]", moveEx, ErrorCode.GenericFailure); } } else { LogLog.Warn(declaringType, "Cannot RollFile [" + fromFile + "] -> [" + toFile + "]. Source does not exist"); } } /// /// Test if a file exists at a specified path /// /// the path to the file /// true if the file exists /// /// /// Test if a file exists at a specified path /// /// protected bool FileExists(string path) { using(SecurityContext.Impersonate(this)) { return System.IO.File.Exists(path); } } /// /// Deletes the specified file if it exists. /// /// The file to delete. /// /// /// Delete a file if is exists. /// The file is first moved to a new filename then deleted. /// This allows the file to be removed even when it cannot /// be deleted, but it still can be moved. /// /// protected void DeleteFile(string fileName) { if (FileExists(fileName)) { // We may not have permission to delete the file, or the file may be locked string fileToDelete = fileName; // Try to move the file to temp name. // If the file is locked we may still be able to move it string tempFileName = fileName + "." + Environment.TickCount + ".DeletePending"; try { using(SecurityContext.Impersonate(this)) { System.IO.File.Move(fileName, tempFileName); } fileToDelete = tempFileName; } catch(Exception moveEx) { LogLog.Debug(declaringType, "Exception while moving file to be deleted [" + fileName + "] -> [" + tempFileName + "]", moveEx); } // Try to delete the file (either the original or the moved file) try { using(SecurityContext.Impersonate(this)) { System.IO.File.Delete(fileToDelete); } LogLog.Debug(declaringType, "Deleted file [" + fileName + "]"); } catch(Exception deleteEx) { if (fileToDelete == fileName) { // Unable to move or delete the file ErrorHandler.Error("Exception while deleting file [" + fileToDelete + "]", deleteEx, ErrorCode.GenericFailure); } else { // Moved the file, but the delete failed. File is probably locked. // The file should automatically be deleted when the lock is released. LogLog.Debug(declaringType, "Exception while deleting temp file [" + fileToDelete + "]", deleteEx); } } } } /// /// Implements file roll base on file size. /// /// /// /// If the maximum number of size based backups is reached /// (curSizeRollBackups == maxSizeRollBackups) then the oldest /// file is deleted -- its index determined by the sign of countDirection. /// If countDirection < 0, then files /// {File.1, ..., File.curSizeRollBackups -1} /// are renamed to {File.2, ..., /// File.curSizeRollBackups}. Moreover, File is /// renamed File.1 and closed. /// /// /// A new file is created to receive further log output. /// /// /// If maxSizeRollBackups is equal to zero, then the /// File is truncated with no backup files created. /// /// /// If maxSizeRollBackups < 0, then File is /// renamed if needed and no files are deleted. /// /// protected void RollOverSize() { this.CloseFile(); // keep windows happy. LogLog.Debug(declaringType, "rolling over count ["+((CountingQuietTextWriter)QuietWriter).Count+"]"); LogLog.Debug(declaringType, "maxSizeRollBackups ["+m_maxSizeRollBackups+"]"); LogLog.Debug(declaringType, "curSizeRollBackups ["+m_curSizeRollBackups+"]"); LogLog.Debug(declaringType, "countDirection ["+m_countDirection+"]"); RollOverRenameFiles(File); if (!m_staticLogFileName && m_countDirection >= 0) { m_curSizeRollBackups++; } // This will also close the file. This is OK since multiple close operations are safe. SafeOpenFile(m_baseFileName, false); } /// /// Implements file roll. /// /// the base name to rename /// /// /// If the maximum number of size based backups is reached /// (curSizeRollBackups == maxSizeRollBackups) then the oldest /// file is deleted -- its index determined by the sign of countDirection. /// If countDirection < 0, then files /// {File.1, ..., File.curSizeRollBackups -1} /// are renamed to {File.2, ..., /// File.curSizeRollBackups}. /// /// /// If maxSizeRollBackups is equal to zero, then the /// File is truncated with no backup files created. /// /// /// If maxSizeRollBackups < 0, then File is /// renamed if needed and no files are deleted. /// /// /// This is called by to rename the files. /// /// protected void RollOverRenameFiles(string baseFileName) { // If maxBackups <= 0, then there is no file renaming to be done. if (m_maxSizeRollBackups != 0) { if (m_countDirection < 0) { // Delete the oldest file, to keep Windows happy. if (m_curSizeRollBackups == m_maxSizeRollBackups) { DeleteFile(CombinePath(baseFileName, "." + m_maxSizeRollBackups)); m_curSizeRollBackups--; } // Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2} for (int i = m_curSizeRollBackups; i >= 1; i--) { RollFile((CombinePath(baseFileName, "." + i)), (CombinePath(baseFileName, "." + (i + 1)))); } m_curSizeRollBackups++; // Rename fileName to fileName.1 RollFile(baseFileName, CombinePath(baseFileName, ".1")); } else { //countDirection >= 0 if (m_curSizeRollBackups >= m_maxSizeRollBackups && m_maxSizeRollBackups > 0) { //delete the first and keep counting up. int oldestFileIndex = m_curSizeRollBackups - m_maxSizeRollBackups; // If static then there is 1 file without a number, therefore 1 less archive if (m_staticLogFileName) { oldestFileIndex++; } // If using a static log file then the base for the numbered sequence is the baseFileName passed in // If not using a static log file then the baseFileName will already have a numbered postfix which // we must remove, however it may have a date postfix which we must keep! string archiveFileBaseName = baseFileName; if (!m_staticLogFileName) { int lastDotIndex = archiveFileBaseName.LastIndexOf("."); if (lastDotIndex >= 0) { archiveFileBaseName = archiveFileBaseName.Substring(0, lastDotIndex); } } // Delete the archive file DeleteFile(CombinePath(archiveFileBaseName, "." + oldestFileIndex)); } if (m_staticLogFileName) { m_curSizeRollBackups++; RollFile(baseFileName, CombinePath(baseFileName, "." + m_curSizeRollBackups)); } } } } #endregion #region NextCheckDate /// /// Get the start time of the next window for the current rollpoint /// /// the current date /// the type of roll point we are working with /// the start time for the next roll point an interval after the currentDateTime date /// /// /// Returns the date of the next roll point after the currentDateTime date passed to the method. /// /// /// The basic strategy is to subtract the time parts that are less significant /// than the rollpoint from the current time. This should roll the time back to /// the start of the time window for the current rollpoint. Then we add 1 window /// worth of time and get the start time of the next window for the rollpoint. /// /// protected DateTime NextCheckDate(DateTime currentDateTime, RollPoint rollPoint) { // Local variable to work on (this does not look very efficient) DateTime current = currentDateTime; // Do slightly different things depending on what the type of roll point we want. switch(rollPoint) { case RollPoint.TopOfMinute: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(1); break; case RollPoint.TopOfHour: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(-current.Minute); current = current.AddHours(1); break; case RollPoint.HalfDay: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(-current.Minute); if (current.Hour < 12) { current = current.AddHours(12 - current.Hour); } else { current = current.AddHours(-current.Hour); current = current.AddDays(1); } break; case RollPoint.TopOfDay: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(-current.Minute); current = current.AddHours(-current.Hour); current = current.AddDays(1); break; case RollPoint.TopOfWeek: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(-current.Minute); current = current.AddHours(-current.Hour); current = current.AddDays(7 - (int)current.DayOfWeek); break; case RollPoint.TopOfMonth: current = current.AddMilliseconds(-current.Millisecond); current = current.AddSeconds(-current.Second); current = current.AddMinutes(-current.Minute); current = current.AddHours(-current.Hour); current = current.AddDays(1 - current.Day); /* first day of month is 1 not 0 */ current = current.AddMonths(1); break; } return current; } #endregion #region Private Instance Fields /// /// This object supplies the current date/time. Allows test code to plug in /// a method to control this class when testing date/time based rolling. The default /// implementation uses the underlying value of DateTime.Now. /// private IDateTime m_dateTime = null; /// /// The date pattern. By default, the pattern is set to ".yyyy-MM-dd" /// meaning daily rollover. /// private string m_datePattern = ".yyyy-MM-dd"; /// /// The actual formatted filename that is currently being written to /// or will be the file transferred to on roll over /// (based on staticLogFileName). /// private string m_scheduledFilename = null; /// /// The timestamp when we shall next recompute the filename. /// private DateTime m_nextCheck = DateTime.MaxValue; /// /// Holds date of last roll over /// private DateTime m_now; /// /// The type of rolling done /// private RollPoint m_rollPoint; /// /// The default maximum file size is 10MB /// private long m_maxFileSize = 10*1024*1024; /// /// There is zero backup files by default /// private int m_maxSizeRollBackups = 0; /// /// How many sized based backups have been made so far /// private int m_curSizeRollBackups = 0; /// /// The rolling file count direction. /// private int m_countDirection = -1; /// /// The rolling mode used in this appender. /// private RollingMode m_rollingStyle = RollingMode.Composite; /// /// Cache flag set if we are rolling by date. /// private bool m_rollDate = true; /// /// Cache flag set if we are rolling by size. /// private bool m_rollSize = true; /// /// Value indicating whether to always log to the same file. /// private bool m_staticLogFileName = true; /// /// Value indicating whether to preserve the file name extension when rolling. /// private bool m_preserveLogFileNameExtension = false; /// /// FileName provided in configuration. Used for rolling properly /// private string m_baseFileName; #if !NETCF /// /// A mutex that is used to lock rolling of files. /// private Mutex m_mutexForRolling; #endif #endregion Private Instance Fields #region Static Members /// /// The 1st of January 1970 in UTC /// private static readonly DateTime s_date1970 = new DateTime(1970, 1, 1); #endregion #region DateTime /// /// This interface is used to supply Date/Time information to the . /// /// /// This interface is used to supply Date/Time information to the . /// Used primarily to allow test classes to plug themselves in so they can /// supply test date/times. /// public interface IDateTime { /// /// Gets the current time. /// /// The current time. /// /// /// Gets the current time. /// /// DateTime Now { get; } } /// /// Default implementation of that returns the current time. /// private class LocalDateTime : IDateTime { /// /// Gets the current time. /// /// The current time. /// /// /// Gets the current time. /// /// public DateTime Now { get { return DateTime.Now; } } } #if !NET_1_0 && !CLI_1_0 && !NETCF /// /// Implementation of that returns the current time as the coordinated universal time (UTC). /// private class UniversalDateTime : IDateTime { /// /// Gets the current time. /// /// The current time. /// /// /// Gets the current time. /// /// public DateTime Now { get { return DateTime.UtcNow; } } } #endif #endregion DateTime } }