From 9118145a4b7f46662f9c3b19acd7dc61b0008e0e Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Thu, 27 Oct 2011 09:43:55 -0700 Subject: [PATCH] Manually applied patch from comment #5 in http://kenai.com/bugzilla/show_bug.cgi?id=4246 --- LogAppenders.cs | 319 +++++++++++++++++++++++++++++++++++++ Main.cs | 118 +------------- PeriodicRollingCalendar.cs | 122 ++++++++++++++ ServiceDescriptor.cs | 89 +++++++---- winsw.csproj | 3 + 5 files changed, 507 insertions(+), 144 deletions(-) create mode 100644 LogAppenders.cs create mode 100644 PeriodicRollingCalendar.cs diff --git a/LogAppenders.cs b/LogAppenders.cs new file mode 100644 index 0000000..114f517 --- /dev/null +++ b/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); + } + + /// + /// Abstraction for handling log. + /// + 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; + } + } + + /// + /// Convenience method to copy stuff from StreamReader to StreamWriter + /// + 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(); + } + + /// + /// File replacement. + /// + 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(); + } + + /// + /// Works like the CopyStream method but does a log rotation based on time. + /// + 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(); + } + + /// + /// Works like the CopyStream method but does a log rotation. + /// + 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(); + } + } + + /// + /// Rotate log when a service is newly started. + /// + 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); + } + } +} \ No newline at end of file diff --git a/Main.cs b/Main.cs index 788b35f..6837837 100644 --- a/Main.cs +++ b/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; } - /// - /// Copy stuff from StreamReader to StreamWriter - /// - 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(); - } - - /// - /// Works like the CopyStream method but does a log rotation. - /// - 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(); - } - /// /// 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) { diff --git a/PeriodicRollingCalendar.cs b/PeriodicRollingCalendar.cs new file mode 100644 index 0000000..94dccbb --- /dev/null +++ b/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); + } + } + + } +} diff --git a/ServiceDescriptor.cs b/ServiceDescriptor.cs index 3a60f2b..f5bb2da 100755 --- a/ServiceDescriptor.cs +++ b/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); + } + } + /// /// Path to the executable. /// @@ -215,25 +229,54 @@ namespace winsw } } - - /// - /// Logmode to 'reset', 'rotate' once or 'append' [default] the out.log and err.log files. - /// - 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); } } + } /// @@ -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); } } diff --git a/winsw.csproj b/winsw.csproj index ced05c1..183ee93 100644 --- a/winsw.csproj +++ b/winsw.csproj @@ -48,9 +48,11 @@ + Component + @@ -58,6 +60,7 @@ +