From 62f13e64cf8d1442834a926be9bc84900bc721f3 Mon Sep 17 00:00:00 2001
From: NextTurn <45985406+nxtn@users.noreply.github.com>
Date: Thu, 30 Apr 2020 00:00:00 +0800
Subject: [PATCH] Tweak stdout/stderr redirections
---
src/WinSW.Core/LogAppenders.cs | 127 ++++++++++++++++++-------
src/WinSW.Tests/LogAppenderTests.cs | 141 ++++++++++++++++++++++++++++
2 files changed, 233 insertions(+), 35 deletions(-)
create mode 100644 src/WinSW.Tests/LogAppenderTests.cs
diff --git a/src/WinSW.Core/LogAppenders.cs b/src/WinSW.Core/LogAppenders.cs
index 543e64c..6f9a81e 100644
--- a/src/WinSW.Core/LogAppenders.cs
+++ b/src/WinSW.Core/LogAppenders.cs
@@ -30,12 +30,12 @@ namespace WinSW
protected override Task LogOutput(StreamReader outputReader)
{
- return this.CopyStreamAsync(outputReader, this.CreateWriter(new FileStream(this.outputPath!, FileMode.OpenOrCreate)));
+ return this.CopyStreamAsync(outputReader.BaseStream, new FileStream(this.outputPath!, FileMode.OpenOrCreate));
}
protected override Task LogError(StreamReader errorReader)
{
- return this.CopyStreamAsync(errorReader, this.CreateWriter(new FileStream(this.errorPath!, FileMode.OpenOrCreate)));
+ return this.CopyStreamAsync(errorReader.BaseStream, new FileStream(this.errorPath!, FileMode.OpenOrCreate));
}
}
@@ -66,12 +66,11 @@ namespace WinSW
///
/// Convenience method to copy stuff from StreamReader to StreamWriter
///
- protected async Task CopyStreamAsync(StreamReader reader, StreamWriter writer)
+ protected async Task CopyStreamAsync(Stream reader, Stream writer)
{
- string? line;
- while ((line = await reader.ReadLineAsync()) != null)
+ var copy = new StreamCopyOperation(reader, writer);
+ while (await copy.CopyLineAsync() != 0)
{
- writer.WriteLine(line);
}
reader.Dispose();
@@ -126,8 +125,6 @@ namespace WinSW
}
}
- protected StreamWriter CreateWriter(FileStream stream) => new StreamWriter(stream) { AutoFlush = true };
-
protected abstract Task LogOutput(StreamReader outputReader);
protected abstract Task LogError(StreamReader errorReader);
@@ -175,12 +172,12 @@ namespace WinSW
protected override Task LogOutput(StreamReader outputReader)
{
- return this.CopyStreamAsync(outputReader, this.CreateWriter(new FileStream(this.OutputLogFileName, this.FileMode)));
+ return this.CopyStreamAsync(outputReader.BaseStream, new FileStream(this.OutputLogFileName, this.FileMode));
}
protected override Task LogError(StreamReader errorReader)
{
- return this.CopyStreamAsync(errorReader, this.CreateWriter(new FileStream(this.ErrorLogFileName, this.FileMode)));
+ return this.CopyStreamAsync(errorReader.BaseStream, new FileStream(this.ErrorLogFileName, this.FileMode));
}
}
@@ -246,17 +243,15 @@ namespace WinSW
var periodicRollingCalendar = new PeriodicRollingCalendar(this.Pattern, this.Period);
periodicRollingCalendar.Init();
- var writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Append));
- string? line;
- while ((line = await reader.ReadLineAsync()) != null)
+ var writer = new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Append);
+ var copy = new StreamCopyOperation(reader.BaseStream, writer);
+ while (await copy.CopyLineAsync() != 0)
{
if (periodicRollingCalendar.ShouldRoll)
{
writer.Dispose();
- writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Create));
+ copy.Writer = writer = new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Create);
}
-
- writer.WriteLine(line);
}
reader.Dispose();
@@ -302,14 +297,15 @@ namespace WinSW
///
private async Task CopyStreamWithRotationAsync(StreamReader reader, string ext)
{
- var writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Append));
+ var writer = new FileStream(this.BaseLogFileName + ext, FileMode.Append);
+ var copy = new StreamCopyOperation(reader.BaseStream, writer);
long fileLength = new FileInfo(this.BaseLogFileName + ext).Length;
- string? line;
- while ((line = await reader.ReadLineAsync()) != null)
+ int written;
+ while ((written = await copy.CopyLineAsync()) != 0)
{
- int lengthToWrite = (line.Length + Environment.NewLine.Length) * sizeof(char);
- if (fileLength + lengthToWrite > this.SizeThreshold)
+ fileLength += written;
+ if (fileLength > this.SizeThreshold)
{
writer.Dispose();
@@ -339,12 +335,9 @@ namespace WinSW
// even if the log rotation fails, create a new one, or else
// we'll infinitely try to roll.
- writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Create));
+ copy.Writer = writer = new FileStream(this.BaseLogFileName + ext, FileMode.Create);
fileLength = new FileInfo(this.BaseLogFileName + ext).Length;
}
-
- writer.WriteLine(line);
- fileLength += lengthToWrite;
}
reader.Dispose();
@@ -432,7 +425,8 @@ namespace WinSW
string? baseFileName = Path.GetFileName(this.BaseLogFileName);
string? logFile = this.BaseLogFileName + extension;
- var writer = this.CreateWriter(new FileStream(logFile, FileMode.Append));
+ var writer = new FileStream(logFile, FileMode.Append);
+ var copy = new StreamCopyOperation(reader.BaseStream, writer);
long fileLength = new FileInfo(logFile).Length;
// We auto roll at time is configured then we need to create a timer and wait until time is elasped and roll the file over
@@ -455,7 +449,7 @@ namespace WinSW
string? nextFileName = Path.Combine(baseDirectory, string.Format("{0}.{1}.#{2:D4}{3}", baseFileName, now.ToString(this.FilePattern), nextFileNumber, extension));
File.Move(logFile, nextFileName);
- writer = this.CreateWriter(new FileStream(logFile, FileMode.Create));
+ copy.Writer = writer = new FileStream(logFile, FileMode.Create);
fileLength = new FileInfo(logFile).Length;
}
@@ -476,13 +470,13 @@ namespace WinSW
timer.Start();
}
- string? line;
- while ((line = await reader.ReadLineAsync()) != null)
+ int written;
+ while ((written = await copy.CopyLineAsync()) != 0)
{
lock (fileLock)
{
- int lengthToWrite = (line.Length + Environment.NewLine.Length) * sizeof(char);
- if (fileLength + lengthToWrite > this.SizeThreshold)
+ fileLength += written;
+ if (fileLength > this.SizeThreshold)
{
try
{
@@ -496,7 +490,7 @@ namespace WinSW
// even if the log rotation fails, create a new one, or else
// we'll infinitely try to roll.
- writer = this.CreateWriter(new FileStream(logFile, FileMode.Create));
+ copy.Writer = writer = new FileStream(logFile, FileMode.Create);
fileLength = new FileInfo(logFile).Length;
}
catch (Exception e)
@@ -504,9 +498,6 @@ namespace WinSW
this.EventLogger.WriteEntry($"Failed to roll size time log: {e.Message}");
}
}
-
- writer.WriteLine(line);
- fileLength += lengthToWrite;
}
}
@@ -633,4 +624,70 @@ namespace WinSW
return nextFileNumber;
}
}
+
+ internal sealed class StreamCopyOperation
+ {
+ private const int BufferSize = 1024;
+
+ private readonly byte[] buffer;
+ private readonly Stream reader;
+
+ private int startIndex;
+ private int endIndex;
+
+ internal Stream Writer;
+
+ internal StreamCopyOperation(Stream reader, Stream writer)
+ {
+ this.buffer = new byte[BufferSize];
+ this.reader = reader;
+ this.startIndex = 0;
+ this.endIndex = 0;
+ this.Writer = writer;
+ }
+
+ internal async Task CopyLineAsync()
+ {
+ byte[] buffer = this.buffer;
+ var source = this.reader;
+ int startIndex = this.startIndex;
+ int endIndex = this.endIndex;
+ var destination = this.Writer;
+
+ int total = 0;
+ while (true)
+ {
+ if (startIndex == 0)
+ {
+ if ((endIndex = await source.ReadAsync(buffer, 0, BufferSize)) == 0)
+ {
+ break;
+ }
+ }
+
+ int buffered = endIndex - startIndex;
+
+ int newLineIndex = Array.IndexOf(buffer, (byte)'\n', startIndex, buffered);
+ if (newLineIndex >= 0)
+ {
+ int count = newLineIndex - startIndex + 1;
+ total += count;
+ destination.Write(buffer, startIndex, count);
+ destination.Flush();
+ startIndex = (newLineIndex + 1) % BufferSize;
+ break;
+ }
+
+ total += buffered;
+ destination.Write(buffer, startIndex, buffered);
+ destination.Flush();
+ startIndex = 0;
+ }
+
+ this.startIndex = startIndex;
+ this.endIndex = endIndex;
+
+ return total;
+ }
+ }
}
diff --git a/src/WinSW.Tests/LogAppenderTests.cs b/src/WinSW.Tests/LogAppenderTests.cs
new file mode 100644
index 0000000..f6887ae
--- /dev/null
+++ b/src/WinSW.Tests/LogAppenderTests.cs
@@ -0,0 +1,141 @@
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Xunit;
+using static System.IO.File;
+
+namespace WinSW.Tests
+{
+ public class LogAppenderTests
+ {
+ private const byte CR = 0x0d;
+ private const byte LF = 0x0a;
+
+ [Fact]
+ public void DefaultLogAppender()
+ {
+ byte[] stdout = { 0x4e, 0x65, 0x78, 0x74 };
+ byte[] stderr = { 0x54, 0x75, 0x72, 0x6e };
+
+ using var data = TestData.Create();
+
+ string baseName = data.name;
+ string outFileExt = ".out.log";
+ string errFileExt = ".err.log";
+ string outFileName = baseName + outFileExt;
+ string errFileName = baseName + errFileExt;
+ string outFilePath = Path.Combine(data.path, outFileName);
+ string errFilePath = Path.Combine(data.path, errFileName);
+
+ WriteAllBytes(outFilePath, stdout);
+ WriteAllBytes(errFilePath, stderr);
+
+ var appender = new DefaultLogAppender(data.path, data.name, false, false, outFileExt, errFileExt);
+ appender.Log(new(new MemoryStream(stdout)), new(new MemoryStream(stderr)));
+
+ Assert.True(Exists(outFilePath));
+ Assert.True(Exists(errFilePath));
+
+ Assert.Equal(stdout.Concat(stdout), ReadAllBytes(outFilePath));
+ Assert.Equal(stderr.Concat(stderr), ReadAllBytes(errFilePath));
+ }
+
+ [Fact]
+ public void ResetLogAppender()
+ {
+ byte[] stdout = { 0x4e, 0x65, 0x78, 0x74 };
+ byte[] stderr = { 0x54, 0x75, 0x72, 0x6e };
+
+ using var data = TestData.Create();
+
+ string baseName = data.name;
+ string outFileExt = ".out.log";
+ string errFileExt = ".err.log";
+ string outFileName = baseName + outFileExt;
+ string errFileName = baseName + errFileExt;
+ string outFilePath = Path.Combine(data.path, outFileName);
+ string errFilePath = Path.Combine(data.path, errFileName);
+
+ WriteAllBytes(outFilePath, stderr);
+ WriteAllBytes(errFilePath, stdout);
+
+ var appender = new ResetLogAppender(data.path, data.name, false, false, outFileExt, errFileExt);
+ appender.Log(new(new MemoryStream(stdout)), new(new MemoryStream(stderr)));
+
+ Assert.True(Exists(outFilePath));
+ Assert.True(Exists(errFilePath));
+
+ Assert.Equal(stdout, ReadAllBytes(outFilePath));
+ Assert.Equal(stderr, ReadAllBytes(errFilePath));
+ }
+
+ [Fact]
+ public void IgnoreLogAppender()
+ {
+ byte[] stdout = { 0x4e, 0x65, 0x78, 0x74 };
+ byte[] stderr = { 0x54, 0x75, 0x72, 0x6e };
+
+ using var data = TestData.Create();
+
+ string baseName = data.name;
+ string outFileExt = ".out.log";
+ string errFileExt = ".err.log";
+ string outFileName = baseName + outFileExt;
+ string errFileName = baseName + errFileExt;
+ string outFilePath = Path.Combine(data.path, outFileName);
+ string errFilePath = Path.Combine(data.path, errFileName);
+
+ var appender = new IgnoreLogAppender();
+ appender.Log(new(new MemoryStream(stdout)), new(new MemoryStream(stderr)));
+
+ Assert.False(Exists(outFilePath));
+ Assert.False(Exists(errFilePath));
+ }
+
+ [Fact]
+ public void SizeBasedRollingLogAppender()
+ {
+ byte[] stdout = { 0x4e, 0x65, CR, LF, 0x78, 0x74 };
+ byte[] stderr = { 0x54, 0x75, CR, LF, 0x72, 0x6e };
+
+ using var data = TestData.Create();
+
+ string baseName = data.name;
+ string outFileExt = ".out.log";
+ string errFileExt = ".err.log";
+
+ var appender = new SizeBasedRollingLogAppender(data.path, data.name, false, false, outFileExt, errFileExt, 3, 2);
+ appender.Log(new(new MemoryStream(stdout)), new(new MemoryStream(stderr)));
+
+ Assert.Equal(stdout.Take(4), ReadAllBytes(Path.Combine(data.path, baseName + ".0" + outFileExt)));
+ Assert.Equal(stdout.Skip(4), ReadAllBytes(Path.Combine(data.path, baseName + outFileExt)));
+ Assert.Equal(stderr.Take(4), ReadAllBytes(Path.Combine(data.path, baseName + ".0" + errFileExt)));
+ Assert.Equal(stderr.Skip(4), ReadAllBytes(Path.Combine(data.path, baseName + errFileExt)));
+ }
+
+ private readonly ref struct TestData
+ {
+ internal readonly string name;
+ internal readonly string path;
+
+ private TestData(string name, string path)
+ {
+ this.name = name;
+ this.path = path;
+ }
+
+ internal static TestData Create([CallerMemberName] string name = null)
+ {
+ string path = Path.Combine(Path.GetTempPath(), name);
+ _ = Directory.CreateDirectory(path);
+
+ return new(name, path);
+ }
+
+ public void Dispose()
+ {
+ Directory.Delete(this.path, true);
+ }
+ }
+ }
+}