Browse Source

Manually applied patch from comment #5 in
http://kenai.com/bugzilla/show_bug.cgi?id=4246

Kohsuke Kawaguchi 14 years ago
parent
commit
9118145a4b
5 changed files with 507 additions and 144 deletions
  1. 319 0
      LogAppenders.cs
  2. 6 112
      Main.cs
  3. 122 0
      PeriodicRollingCalendar.cs
  4. 57 32
      ServiceDescriptor.cs
  5. 3 0
      winsw.csproj

+ 319 - 0
LogAppenders.cs

@@ -0,0 +1,319 @@
+using System.IO;
+using System.Diagnostics;
+using System.Threading;
+
+namespace winsw
+{
+    public interface EventLogger
+    {
+        void LogEvent(string message);
+        void LogEvent(string message, EventLogEntryType type);
+    }
+
+    /// <summary>
+    /// Abstraction for handling log.
+    /// </summary>
+    public abstract class LogHandler
+    {
+        private EventLogger eventLogger;
+        private string baseLogFileName;
+
+        public LogHandler(string logDirectory, string baseName)
+        {
+            this.baseLogFileName = Path.Combine(logDirectory, baseName);
+        }
+
+        public abstract void log(Stream outputStream, Stream errorStream);
+
+        public EventLogger EventLogger
+        {
+            set
+            {
+                this.eventLogger = value;
+            }
+            get
+            {
+                return this.eventLogger;
+            }
+        }
+
+        public string BaseLogFileName
+        {
+            get
+            {
+                return this.baseLogFileName;
+            }
+        }
+
+        /// <summary>
+        /// Convenience method to copy stuff from StreamReader to StreamWriter
+        /// </summary>
+        protected void CopyStream(Stream i, Stream o)
+        {
+            byte[] buf = new byte[1024];
+            while (true)
+            {
+                int sz = i.Read(buf, 0, buf.Length);
+                if (sz == 0) break;
+                o.Write(buf, 0, sz);
+                o.Flush();
+            }
+            i.Close();
+            o.Close();
+        }
+
+        /// <summary>
+        /// File replacement.
+        /// </summary>
+        protected void CopyFile(string sourceFileName, string destFileName)
+        {
+            try
+            {
+                File.Delete(destFileName);
+                File.Move(sourceFileName, destFileName);
+            }
+            catch (IOException e)
+            {
+                EventLogger.LogEvent("Failed to copy :" + sourceFileName + " to " + destFileName + " because " + e.Message);
+            }
+        }
+    }
+
+    public abstract class SimpleLogAppender : LogHandler
+    {
+
+        private FileMode fileMode;
+        private string outputLogFileName;
+        private string errorLogFileName;
+
+        public SimpleLogAppender(string logDirectory, string baseName, FileMode fileMode)
+            : base(logDirectory, baseName)
+        {
+            this.fileMode = fileMode;
+            this.outputLogFileName = BaseLogFileName + ".out.log";
+            this.errorLogFileName = BaseLogFileName + ".err.log";
+        }
+
+        public string OutputLogFileName
+        {
+            get
+            {
+                return this.outputLogFileName;
+            }
+        }
+
+        public string ErrorLogFileName
+        {
+            get
+            {
+                return this.errorLogFileName;
+            }
+        }
+
+        public override void log(Stream outputStream, Stream errorStream)
+        {
+            new Thread(delegate() { CopyStream(outputStream, new FileStream(outputLogFileName, fileMode)); }).Start();
+            new Thread(delegate() { CopyStream(errorStream, new FileStream(errorLogFileName, fileMode)); }).Start();
+        }
+    }
+
+    public class DefaultLogAppender : SimpleLogAppender
+    {
+        public DefaultLogAppender(string logDirectory, string baseName)
+            : base(logDirectory, baseName, FileMode.Append)
+        {
+        }
+    }
+
+    public class ResetLogAppender : SimpleLogAppender
+    {
+        public ResetLogAppender(string logDirectory, string baseName)
+            : base(logDirectory, baseName, FileMode.Create)
+        {
+        }
+
+    }
+
+    public class TimeBasedRollingLogAppender : LogHandler
+    {
+
+        private string pattern;
+        private int period;
+
+        public TimeBasedRollingLogAppender(string logDirectory, string baseName, string pattern, int period)
+            : base(logDirectory, baseName)
+        {
+            this.pattern = pattern;
+            this.period = period;
+        }
+
+        public override void log(Stream outputStream, Stream errorStream)
+        {
+            new Thread(delegate() { CopyStreamWithDateRotation(outputStream, ".out.log"); }).Start();
+            new Thread(delegate() { CopyStreamWithDateRotation(errorStream, ".err.log"); }).Start();
+        }
+
+        /// <summary>
+        /// Works like the CopyStream method but does a log rotation based on time.
+        /// </summary>
+        private void CopyStreamWithDateRotation(Stream data, string ext)
+        {
+            PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(pattern, period);
+            periodicRollingCalendar.init();
+
+            byte[] buf = new byte[1024];
+            FileStream w = new FileStream(BaseLogFileName + "_" + periodicRollingCalendar.format + ext, FileMode.Create);
+            while (true)
+            {
+                int len = data.Read(buf, 0, buf.Length);
+                if (len == 0) break;    // EOF
+
+                if (periodicRollingCalendar.shouldRoll)
+                {// rotate at the line boundary
+                    int offset = 0;
+                    bool rolled = false;
+                    for (int i = 0; i < len; i++)
+                    {
+                        if (buf[i] == 0x0A)
+                        {// at the line boundary.
+                            // time to rotate.
+                            w.Write(buf, offset, i + 1);
+                            w.Close();
+                            offset = i + 1;
+
+                            // create a new file.
+                            w = new FileStream(BaseLogFileName + "_" + periodicRollingCalendar.format + ext, FileMode.Create);
+                            rolled = true;
+                        }
+                    }
+
+                    if (!rolled)
+                    {// we didn't roll - most likely as we didnt find a line boundary, so we should log what we read and roll anyway.
+                        w.Write(buf, 0, len);
+                        w.Close();
+                        w = new FileStream(BaseLogFileName + "_" + periodicRollingCalendar.format + ext, FileMode.Create);
+                    }
+
+                }
+                else
+                {// typical case. write the whole thing into the current file
+                    w.Write(buf, 0, len);
+                }
+
+                w.Flush();
+            }
+            data.Close();
+            w.Close();
+        }
+
+    }
+
+    public class SizeBasedRollingLogAppender : LogHandler
+    {
+        public static int BYTES_PER_KB = 1024;
+        public static int BYTES_PER_MB = 1024 * BYTES_PER_KB;
+        public static int DEFAULT_SIZE_THRESHOLD = 10 * BYTES_PER_MB; // rotate every 10MB.
+        public static int DEFAULT_FILES_TO_KEEP = 8;
+
+        private int sizeThreshold;
+        private int filesToKeep;
+
+        public SizeBasedRollingLogAppender(string logDirectory, string baseName, int sizeThreshold, int filesToKeep)
+            : base(logDirectory, baseName)
+        {
+            this.sizeThreshold = sizeThreshold;
+            this.filesToKeep = filesToKeep;
+        }
+
+        public SizeBasedRollingLogAppender(string logDirectory, string baseName)
+            : this(logDirectory, baseName, DEFAULT_SIZE_THRESHOLD, DEFAULT_FILES_TO_KEEP) { }
+
+        public override void log(Stream outputStream, Stream errorStream)
+        {
+            new Thread(delegate() { CopyStreamWithRotation(outputStream, ".out.log"); }).Start();
+            new Thread(delegate() { CopyStreamWithRotation(errorStream, ".err.log"); }).Start();
+        }
+
+        /// <summary>
+        /// Works like the CopyStream method but does a log rotation.
+        /// </summary>
+        private void CopyStreamWithRotation(Stream data, string ext)
+        {
+            byte[] buf = new byte[1024];
+            FileStream w = new FileStream(BaseLogFileName + ext, FileMode.Append);
+            long sz = new FileInfo(BaseLogFileName + ext).Length;
+
+            while (true)
+            {
+                int len = data.Read(buf, 0, buf.Length);
+                if (len == 0) break;    // EOF
+                if (sz + len < sizeThreshold)
+                {// typical case. write the whole thing into the current file
+                    w.Write(buf, 0, len);
+                    sz += len;
+                }
+                else
+                {
+                    // rotate at the line boundary
+                    int s = 0;
+                    for (int i = 0; i < len; i++)
+                    {
+                        if (buf[i] != 0x0A) continue;
+                        if (sz + i < sizeThreshold) continue;
+
+                        // at the line boundary and exceeded the rotation unit.
+                        // time to rotate.
+                        w.Write(buf, s, i + 1);
+                        w.Close();
+                        s = i + 1;
+
+                        try
+                        {
+                            for (int j = filesToKeep; j >= 1; j--)
+                            {
+                                string dst = BaseLogFileName + "." + (j - 1) + ext;
+                                string src = BaseLogFileName + "." + (j - 2) + ext;
+                                if (File.Exists(dst))
+                                    File.Delete(dst);
+                                if (File.Exists(src))
+                                    File.Move(src, dst);
+                            }
+                            File.Move(BaseLogFileName + ext, BaseLogFileName + ".0" + ext);
+                        }
+                        catch (IOException e)
+                        {
+                            EventLogger.LogEvent("Failed to rotate log: " + e.Message);
+                        }
+
+                        // even if the log rotation fails, create a new one, or else
+                        // we'll infinitely try to rotate.
+                        w = new FileStream(BaseLogFileName + ext, FileMode.Create);
+                        sz = new FileInfo(BaseLogFileName + ext).Length;
+                    }
+                }
+
+                w.Flush();
+            }
+            data.Close();
+            w.Close();
+        }
+    }
+
+    /// <summary>
+    /// Rotate log when a service is newly started.
+    /// </summary>
+    public class RollingLogAppender : SimpleLogAppender
+    {
+        public RollingLogAppender(string logDirectory, string baseName)
+            : base(logDirectory, baseName, FileMode.Append)
+        {
+        }
+
+        public override void log(Stream outputStream, Stream errorStream)
+        {
+            CopyFile(OutputLogFileName, OutputLogFileName + ".old");
+            CopyFile(ErrorLogFileName, ErrorLogFileName + ".old");
+            base.log(outputStream, errorStream);
+        }
+    }
+}

+ 6 - 112
Main.cs

@@ -37,7 +37,7 @@ namespace winsw
         SERVICE_PAUSED = 0x00000007,
     }
     
-    public class WrapperService : ServiceBase
+    public class WrapperService : ServiceBase, EventLogger
     {
         [DllImport("ADVAPI32.DLL")]
         private static extern bool SetServiceStatus(IntPtr hServiceStatus, ref SERVICE_STATUS lpServiceStatus);
@@ -69,89 +69,6 @@ namespace winsw
             this.systemShuttingdown = false;
         }
 
-        /// <summary>
-        /// Copy stuff from StreamReader to StreamWriter
-        /// </summary>
-        private void CopyStream(Stream i, Stream o)
-        {
-            byte[] buf = new byte[1024];
-            while (true)
-            {
-                int sz = i.Read(buf, 0, buf.Length);
-                if (sz == 0) break;
-                o.Write(buf, 0, sz);
-                o.Flush();
-            }
-            i.Close();
-            o.Close();
-        }
-
-        /// <summary>
-        /// Works like the CopyStream method but does a log rotation.
-        /// </summary>
-        private void CopyStreamWithRotation(Stream data, string baseName, string ext)
-        {
-            int THRESHOLD = 10 * 1024 * 1024; // rotate every 10MB. should be made configurable.
-
-            byte[] buf = new byte[1024];
-            FileStream w = new FileStream(baseName + ext, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
-            long sz = new FileInfo(baseName + ext).Length;
-
-            while (true)
-            {
-                int len = data.Read(buf, 0, buf.Length);
-                if (len == 0) break;    // EOF
-                if (sz + len < THRESHOLD)
-                {// typical case. write the whole thing into the current file
-                    w.Write(buf, 0, len);
-                    sz += len;
-                }
-                else
-                {
-                    // rotate at the line boundary
-                    int s = 0;
-                    for (int i = 0; i < len; i++)
-                    {
-                        if (buf[i] != 0x0A) continue;
-                        if (sz + i < THRESHOLD) continue;
-
-                        // at the line boundary and exceeded the rotation unit.
-                        // time to rotate.
-                        w.Write(buf, s, i + 1);
-                        w.Close();
-                        s = i + 1;
-
-                        try
-                        {
-                            for (int j = 8; j >= 0; j--)
-                            {
-                                string dst = baseName + "." + (j + 1) + ext;
-                                string src = baseName + "." + (j + 0) + ext;
-                                if (File.Exists(dst))
-                                    File.Delete(dst);
-                                if (File.Exists(src))
-                                    File.Move(src, dst);
-                            }
-                            File.Move(baseName + ext, baseName + ".0" + ext);
-                        }
-                        catch (IOException e)
-                        {
-                            LogEvent("Failed to rotate log: " + e.Message);
-                        }
-
-                        // even if the log rotation fails, create a new one, or else
-                        // we'll infinitely try to rotate.
-                        w = new FileStream(baseName + ext, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
-                        sz = new FileInfo(baseName + ext).Length;
-                    }
-                }
-
-                w.Flush();
-            }
-            data.Close();
-            w.Close();
-        }
-
         /// <summary>
         /// Process the file copy instructions, so that we can replace files that are always in use while
         /// the service runs.
@@ -235,35 +152,12 @@ namespace winsw
                 Directory.CreateDirectory(logDirectory);
             }
 
-            string baseName = descriptor.BaseName;
-            string errorLogfilename = Path.Combine(logDirectory, baseName + ".err.log");
-            string outputLogfilename = Path.Combine(logDirectory, baseName + ".out.log");
-
-            if (descriptor.Logmode == "rotate")
-            {
-                string logName = Path.Combine(logDirectory, baseName);
-                StartThread(delegate() { CopyStreamWithRotation(process.StandardOutput.BaseStream, logName, ".out.log"); });
-                StartThread(delegate() { CopyStreamWithRotation(process.StandardError.BaseStream, logName, ".err.log"); });
-                return;
-            }
-
-            FileMode fileMode = FileMode.Append;
-
-            if (descriptor.Logmode == "reset")
-            {
-                fileMode = FileMode.Create;
-            }
-            else if (descriptor.Logmode == "roll")
-            {
-                CopyFile(outputLogfilename, outputLogfilename + ".old");
-                CopyFile(errorLogfilename, errorLogfilename + ".old");
-            }
-
-            StartThread(delegate() { CopyStream(process.StandardOutput.BaseStream, new FileStream(outputLogfilename, fileMode, FileAccess.Write, FileShare.ReadWrite)); });
-            StartThread(delegate() { CopyStream(process.StandardError.BaseStream, new FileStream(errorLogfilename, fileMode, FileAccess.Write, FileShare.ReadWrite)); });
+            LogHandler logAppender = descriptor.LogHandler;
+            logAppender.EventLogger = this;
+            logAppender.log(process.StandardOutput.BaseStream, process.StandardError.BaseStream);
         }
 
-        private void LogEvent(String message)
+        public void LogEvent(String message)
         {
             if (systemShuttingdown)
             {
@@ -275,7 +169,7 @@ namespace winsw
             }
         }
 
-        private void LogEvent(String message, EventLogEntryType type)
+        public void LogEvent(String message, EventLogEntryType type)
         {
             if (systemShuttingdown)
             {

+ 122 - 0
PeriodicRollingCalendar.cs

@@ -0,0 +1,122 @@
+using System;
+using System.Data;
+
+namespace winsw
+{
+    /**
+     *  This is largely borrowed from the logback Rolling Calendar.
+     **/
+    public class PeriodicRollingCalendar
+    {
+        private PeriodicityType _periodicityType;
+        private string _format;
+        private long _period;
+        private DateTime _currentRoll;
+        private DateTime _nextRoll;
+
+        public PeriodicRollingCalendar(string format, long period)
+        {
+            this._format = format;
+            this._period = period;
+            this._currentRoll = DateTime.Now;
+        }
+
+        public void init()
+        {
+            this._periodicityType = determinePeriodicityType();
+            this._nextRoll = nextTriggeringTime(this._currentRoll);
+        }
+
+        public enum PeriodicityType
+        {
+            ERRONEOUS, TOP_OF_MILLISECOND, TOP_OF_SECOND, TOP_OF_MINUTE, TOP_OF_HOUR, TOP_OF_DAY
+        }
+
+        private static PeriodicityType[] VALID_ORDERED_LIST = new PeriodicityType[] {
+            PeriodicityType.TOP_OF_MILLISECOND, PeriodicityType.TOP_OF_SECOND, PeriodicityType.TOP_OF_MINUTE, PeriodicityType.TOP_OF_HOUR, PeriodicityType.TOP_OF_DAY
+        };
+
+        private PeriodicityType determinePeriodicityType()
+        {
+            PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(_format, _period);
+            DateTime epoch = new DateTime(1970, 1, 1);
+
+            foreach (PeriodicityType i in VALID_ORDERED_LIST)
+            {
+                string r0 = epoch.ToString(_format);
+                periodicRollingCalendar.periodicityType = i;
+
+                DateTime next = periodicRollingCalendar.nextTriggeringTime(epoch);
+                string r1 = next.ToString(_format);
+
+                if (r0 != null && r1 != null && !r0.Equals(r1))
+                {
+                    return i;
+                }
+            }
+            return PeriodicityType.ERRONEOUS;
+        }
+
+        private DateTime nextTriggeringTime(DateTime input)
+        {
+            DateTime output;
+            switch (_periodicityType)
+            {
+                case PeriodicityType.TOP_OF_MILLISECOND:
+                    output = new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second, input.Millisecond);
+                    output = output.AddMilliseconds(_period);
+                    return output;
+                case PeriodicityType.TOP_OF_SECOND:
+                    output = new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second);
+                    output = output.AddSeconds(_period);
+                    return output;
+                case PeriodicityType.TOP_OF_MINUTE:
+                    output = new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, 0);
+                    output = output.AddMinutes(_period);
+                    return output;
+                case PeriodicityType.TOP_OF_HOUR:
+                    output = new DateTime(input.Year, input.Month, input.Day, input.Hour, 0, 0);
+                    output = output.AddHours(_period);
+                    return output;
+                case PeriodicityType.TOP_OF_DAY:
+                    output = new DateTime(input.Year, input.Month, input.Day);
+                    output = output.AddDays(_period);
+                    return output;
+                default:
+                    throw new Exception("invalid periodicity type: " + _periodicityType);
+            }
+        }
+
+        public PeriodicityType periodicityType
+        {
+            set
+            {
+                this._periodicityType = value;
+            }
+        }
+
+        public Boolean shouldRoll
+        {
+            get
+            {
+                DateTime now = DateTime.Now;
+                if (now > this._nextRoll)
+                {
+                    this._currentRoll = now;
+                    this._nextRoll = nextTriggeringTime(now);
+                    return true;
+                }
+                return false;
+            }
+        }
+
+        public string format
+        {
+            get
+            {
+                return this._currentRoll.ToString(this._format);
+            }
+        }
+
+    }
+}

+ 57 - 32
ServiceDescriptor.cs

@@ -76,6 +76,20 @@ namespace winsw
             return Environment.ExpandEnvironmentVariables(n.InnerText);
         }
 
+        private int SingleIntElement(XmlNode parent, string tagName, int defaultValue)
+        {
+            var e = parent.SelectSingleNode(tagName);
+
+            if (e == null)
+            {
+                return defaultValue;
+            }
+            else
+            {
+                return int.Parse(e.InnerText);
+            }
+        }
+
         /// <summary>
         /// Path to the executable.
         /// </summary>
@@ -215,25 +229,54 @@ namespace winsw
             }
         }
 
-
-        /// <summary>
-        /// Logmode to 'reset', 'rotate' once or 'append' [default] the out.log and err.log files.
-        /// </summary>
-        public string Logmode
+        public LogHandler LogHandler
         {
             get
             {
-                XmlNode logmodeNode = dom.SelectSingleNode("//logmode");
-
-                if (logmodeNode == null)
-                {
-                    return "append";
+                string mode;
+                
+                // first, backward compatibility with older configuration
+                XmlElement e = (XmlElement)dom.SelectSingleNode("//logmode");
+                if (e!=null) {
+                    mode = e.InnerText;
+                } else {
+                    // this is more modern way, to support nested elements as configuration
+                    e = (XmlElement)dom.SelectSingleNode("//log");
+                    mode = e.GetAttribute("mode");
                 }
-                else
+
+                switch (mode)
                 {
-                    return logmodeNode.InnerText;
+                    case "rotate":
+                        return new SizeBasedRollingLogAppender(LogDirectory, BaseName);
+
+                    case "reset":
+                        return new ResetLogAppender(LogDirectory, BaseName);
+
+                    case "roll":
+                        return new RollingLogAppender(LogDirectory, BaseName);
+
+                    case "roll-by-time":
+                        XmlNode patternNode = e.SelectSingleNode("pattern");
+                        if (patternNode == null)
+                        {
+                            throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML.");
+                        }
+                        string pattern = patternNode.InnerText;
+                        int period = SingleIntElement(e,"period",1);
+                        return new TimeBasedRollingLogAppender(LogDirectory, BaseName, pattern, period);
+
+                    case "roll-by-size":
+                        int sizeThreshold = SingleIntElement(e,"sizeThreshold",10*1024)  * SizeBasedRollingLogAppender.BYTES_PER_KB;
+                        int keepFiles = SingleIntElement(e,"keepFiles",SizeBasedRollingLogAppender.DEFAULT_FILES_TO_KEEP);
+                        return new SizeBasedRollingLogAppender(LogDirectory, BaseName, sizeThreshold, keepFiles);
+
+                    case "append":
+                    default:
+                        return new DefaultLogAppender(LogDirectory, BaseName);
                 }
             }
+
         }
 
         /// <summary>
@@ -299,16 +342,7 @@ namespace winsw
         {
             get
             {
-                XmlNode waithintNode = dom.SelectSingleNode("//waithint");
-
-                if (waithintNode == null)
-                {
-                    return 15000;
-                }
-                else
-                {
-                    return int.Parse(waithintNode.InnerText);
-                }
+                return SingleIntElement(dom, "waithint", 15000);
             }
         }
 
@@ -322,16 +356,7 @@ namespace winsw
         {
             get
             {
-                XmlNode sleeptimeNode = dom.SelectSingleNode("//sleeptime");
-
-                if (sleeptimeNode == null)
-                {
-                    return 1000;
-                }
-                else
-                {
-                    return int.Parse(sleeptimeNode.InnerText);
-                }
+                return SingleIntElement(dom, "sleeptime", 15000);
             }
         }
 

+ 3 - 0
winsw.csproj

@@ -48,9 +48,11 @@
   <ItemGroup>
     <Compile Include="Download.cs" />
     <Compile Include="DynamicProxy.cs" />
+    <Compile Include="LogAppenders.cs" />
     <Compile Include="Main.cs">
       <SubType>Component</SubType>
     </Compile>
+    <Compile Include="PeriodicRollingCalendar.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ServiceDescriptor.cs" />
     <Compile Include="Wmi.cs" />
@@ -58,6 +60,7 @@
   </ItemGroup>
   <ItemGroup>
     <Content Include="manifest.xml" />
+    <Content Include="pom.xml" />
     <Content Include="winsw.xml" />
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />