Manually applied patch from comment #5 in

http://kenai.com/bugzilla/show_bug.cgi?id=4246
pull/4/head
Kohsuke Kawaguchi 2011-10-27 09:43:55 -07:00
parent f211481e77
commit 9118145a4b
5 changed files with 507 additions and 144 deletions

319
LogAppenders.cs Normal file
View File

@ -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);
}
}
}

118
Main.cs
View File

@ -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;
LogHandler logAppender = descriptor.LogHandler;
logAppender.EventLogger = this;
logAppender.log(process.StandardOutput.BaseStream, process.StandardError.BaseStream);
}
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)); });
}
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
PeriodicRollingCalendar.cs Normal file
View File

@ -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);
}
}
}
}

View File

@ -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");
string mode;
if (logmodeNode == null)
// 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");
}
switch (mode)
{
return "append";
}
else
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)
{
return logmodeNode.InnerText;
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);
}
}

View File

@ -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" />