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