diff --git a/src/integration-testing/log4net-611-lib/DerivedAppender.cs b/src/integration-testing/log4net-611-lib/DerivedAppender.cs index 2b1faebd..20fa3f16 100644 --- a/src/integration-testing/log4net-611-lib/DerivedAppender.cs +++ b/src/integration-testing/log4net-611-lib/DerivedAppender.cs @@ -1,14 +1,13 @@ using log4net.Appender; using log4net.Core; -using log4net.Layout; namespace log4net_611_lib; -public class DerivedAppender: RollingFileAppender +public class DerivedAppender : RollingFileAppender { - protected override void Append(LoggingEvent loggingEvent) - { - loggingEvent.Properties["appender-class-name"] = nameof(DerivedAppender); - base.Append(loggingEvent); - } + protected override void Append(LoggingEvent loggingEvent) + { + loggingEvent.Properties["appender-class-name"] = nameof(DerivedAppender); + base.Append(loggingEvent); + } } \ No newline at end of file diff --git a/src/integration-testing/log4net-611-main/Program.cs b/src/integration-testing/log4net-611-main/Program.cs index 36f09762..bc316376 100644 --- a/src/integration-testing/log4net-611-main/Program.cs +++ b/src/integration-testing/log4net-611-main/Program.cs @@ -1,7 +1,6 @@ using System.Reflection; using log4net; using log4net.Config; -using log4net_611_lib; var appPath = new Uri(Assembly.GetExecutingAssembly().Location).LocalPath; var appFolder = Path.GetDirectoryName(appPath); @@ -9,7 +8,7 @@ Assembly.LoadFile("log4net-611-lib.dll"); if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } Assembly.LoadFile(Path.Combine(appFolder, "log4net-611-lib.dll")); @@ -17,36 +16,33 @@ var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("Logs")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("Logs")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("Logs")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); Console.WriteLine("logging..."); for (var i = 0; i < 10; i++) { - logger.Info($"test log {i}"); + logger.Info($"test log {i}"); } foreach (var file in Directory.EnumerateFiles("Logs")) { - Console.WriteLine($"log file: {file}"); - Console.WriteLine(File.ReadAllText(file)); + Console.WriteLine($"log file: {file}"); + Console.WriteLine(File.ReadAllText(file)); } \ No newline at end of file diff --git a/src/integration-testing/log4net-672/Program.cs b/src/integration-testing/log4net-672/Program.cs index 79f47611..720c3774 100644 --- a/src/integration-testing/log4net-672/Program.cs +++ b/src/integration-testing/log4net-672/Program.cs @@ -14,43 +14,41 @@ var appFolder = Path.GetDirectoryName(appPath); if (appFolder is null) { - throw new InvalidOperationException( - $"Can't determine app folder for {appPath}" - ); + throw new InvalidOperationException($"Can't determine app folder for {appPath}"); } var logFolder = Path.Combine(appFolder, "Logs"); if (Directory.Exists(logFolder)) { - Directory.Delete(logFolder, recursive: true); + Directory.Delete(logFolder, recursive: true); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } var logCount = 10; var identifiers = new List(); for (var i = 0; i < 10; i++) { - var identifier = Guid.NewGuid(); - identifiers.Add(identifier); - var logged = LogWith(identifier, logCount); - if (logged != logCount) - { - Die($"Missing logs immediately for '{identifier}' - found {logged}/{logCount}", MISSING_LOGS); - } + var identifier = Guid.NewGuid(); + identifiers.Add(identifier); + var logged = LogWith(identifier, logCount); + if (logged != logCount) + { + Die($"Missing logs immediately for '{identifier}' - found {logged}/{logCount}", MISSING_LOGS); + } } foreach (var identifier in identifiers) { - var logged = CountIdentifierInLogs(identifier); - if (logged != logCount) - { - Die($"Logs have been overwritten for '{identifier}' - found {logged}/{logCount}", OVERWRITTEN_LOGS); - } + var logged = CountIdentifierInLogs(identifier); + if (logged != logCount) + { + Die($"Logs have been overwritten for '{identifier}' - found {logged}/{logCount}", OVERWRITTEN_LOGS); + } } Console.WriteLine("All good: LOG4NET-672 is resolved"); @@ -58,34 +56,34 @@ void Die(string message, int exitCode) { - Console.Error.WriteLine(message); - Environment.Exit(exitCode); + Console.Error.WriteLine(message); + Environment.Exit(exitCode); } int CountIdentifierInLogs(Guid id) { - return Directory.EnumerateFiles("Logs").Select( - filePath => CountIdentifierInFile(id, filePath) - ).Sum(); + return Directory.EnumerateFiles("Logs").Select( + filePath => CountIdentifierInFile(id, filePath) + ).Sum(); } int CountIdentifierInFile(Guid id, string filePath) { - var contents = File.ReadAllLines(filePath); - return contents.Count(line => line.Contains(id.ToString())); + var contents = File.ReadAllLines(filePath); + return contents.Count(line => line.Contains(id.ToString())); } int LogWith(Guid identifier, int howManyLogs) { - var info = new FileInfo(configFile); - XmlConfigurator.Configure(info); - var logger = LogManager.GetLogger("main"); + var info = new FileInfo(configFile); + XmlConfigurator.Configure(info); + var logger = LogManager.GetLogger("main"); - for (var i = 0; i < howManyLogs; i++) - { - logger.Info($"test log {i} [{identifier}]"); - } + for (var i = 0; i < howManyLogs; i++) + { + logger.Info($"test log {i} [{identifier}]"); + } - LogManager.Flush(int.MaxValue); - return CountIdentifierInLogs(identifier); + LogManager.Flush(int.MaxValue); + return CountIdentifierInLogs(identifier); } \ No newline at end of file diff --git a/src/integration-testing/log4net-673/Program.cs b/src/integration-testing/log4net-673/Program.cs index fb910898..a3d50414 100644 --- a/src/integration-testing/log4net-673/Program.cs +++ b/src/integration-testing/log4net-673/Program.cs @@ -10,68 +10,65 @@ // force loading the assembly, otherwise the appender type isn't found later if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("log")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("log")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("log")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); Console.WriteLine("logging..."); for (var i = 0; i < 10; i++) { - logger.Info($"test log {i}"); - logger.Error($"error log {i}"); - logger.Warn($"warning log {i}"); + logger.Info($"test log {i}"); + logger.Error($"error log {i}"); + logger.Warn($"warning log {i}"); } foreach (var file in Directory.EnumerateFiles("log")) { - Console.WriteLine($"log file: {file}"); - TryDumpFile(file); + Console.WriteLine($"log file: {file}"); + TryDumpFile(file); } -void TryDumpFile(string at) +static void TryDumpFile(string at) { - if (!File.Exists(at)) + if (!File.Exists(at)) + { + Console.WriteLine($"File not found: {at}"); + return; + } + + for (var i = 0; i < 10; i++) + { + try { - Console.WriteLine($"File not found: {at}"); - return; + Console.WriteLine(File.ReadAllText(at)); + return; } - - for (var i = 0; i < 10; i++) + catch { - try - { - Console.WriteLine(File.ReadAllText(at)); - return; - } - catch - { - Thread.Sleep(100); - } + Thread.Sleep(100); } + } - Console.WriteLine($"Unable to read file at {at}"); -} + Console.WriteLine($"Unable to read file at {at}"); +} \ No newline at end of file diff --git a/src/integration-testing/log4net-681/Program.cs b/src/integration-testing/log4net-681/Program.cs index 20d6bc12..fac4e47a 100644 --- a/src/integration-testing/log4net-681/Program.cs +++ b/src/integration-testing/log4net-681/Program.cs @@ -7,30 +7,27 @@ if (appFolder is null) { - throw new InvalidOperationException("Can't find myself"); + throw new InvalidOperationException("Can't find myself"); } var configFile = Path.Combine(appFolder, "log4net.config"); if (!File.Exists(configFile)) { - throw new InvalidOperationException($"log4net.config not found at {configFile}"); + throw new InvalidOperationException($"log4net.config not found at {configFile}"); } if (Directory.Exists("Logs")) { - Console.WriteLine("Clearing out old logs..."); - foreach (var file in Directory.EnumerateFiles("Logs")) - { - File.Delete(file); - } + Console.WriteLine("Clearing out old logs..."); + foreach (var file in Directory.EnumerateFiles("Logs")) + { + File.Delete(file); + } } var info = new FileInfo(configFile); var logRepo = LogManager.GetRepository(Assembly.GetExecutingAssembly()); -XmlConfigurator.ConfigureAndWatch( - logRepo, - info -); +XmlConfigurator.ConfigureAndWatch(logRepo, info); var logger = LogManager.GetLogger(typeof(Program)); @@ -38,25 +35,25 @@ var threads = new List(); for (var i = 0; i < 128; i++) { - var thread = new Thread(LogABit); - thread.Start(); - threads.Add(thread); + var thread = new Thread(LogABit); + thread.Start(); + threads.Add(thread); } foreach (var t in threads) { - t.Join(); + t.Join(); } foreach (var file in Directory.EnumerateFiles("Logs")) { - Console.WriteLine($"found log file: {file}"); + Console.WriteLine($"found log file: {file}"); } void LogABit() { - for (var i = 0; i < 100; i++) - { - logger.Info($"test log {i}"); - } + for (var i = 0; i < 100; i++) + { + logger.Info($"test log {i}"); + } } \ No newline at end of file diff --git a/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs b/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs new file mode 100644 index 00000000..adcd0e4e --- /dev/null +++ b/src/log4net.Tests/Util/EncodingWithoutPreambleTest.cs @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using log4net.Util; +using NUnit.Framework; + +namespace log4net.Tests.Util; + +/// +/// Tests for +/// +[TestFixture] +public sealed class EncodingWithoutPreambleTest +{ + /// + /// Tests the wrapping functionality + /// + [Test] + public void WrappedTest() + { + Encoding wrapped = Encoding.UTF8; + Type encodingType = typeof(LogLog).Assembly.GetType("log4net.Util.EncodingWithoutPreamble", true)!; + Encoding target = (Encoding)encodingType + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + .First() + .Invoke(new[] { wrapped }); + Assert.IsTrue(target.Equals(wrapped)); + const string text = "Hallöchen!"; + byte[] bytes = wrapped.GetBytes(text); + Assert.AreEqual(bytes, target.GetBytes(text)); + Assert.AreEqual(wrapped.GetString(bytes), target.GetString(bytes)); + CollectionAssert.AreEqual(new byte[] { 0xEF, 0xBB, 0xBF }, wrapped.GetPreamble()); + CollectionAssert.AreEqual(Array.Empty(), target.GetPreamble()); + } +} \ No newline at end of file diff --git a/src/log4net/Appender/ColoredConsoleAppender.cs b/src/log4net/Appender/ColoredConsoleAppender.cs index c5c43915..e4c208c3 100644 --- a/src/log4net/Appender/ColoredConsoleAppender.cs +++ b/src/log4net/Appender/ColoredConsoleAppender.cs @@ -20,471 +20,464 @@ using System; using System.IO; using System.Runtime.InteropServices; - +using System.Text; using log4net.Core; -using log4net.Layout; using log4net.Util; -namespace log4net.Appender +namespace log4net.Appender; + +/// +/// Appends logging events to the console. +/// +/// +/// +/// ColoredConsoleAppender appends log events to the standard output stream +/// or the error output stream using a layout specified by the +/// user. It also allows the color of a specific type of message to be set. +/// +/// +/// By default, all output is written to the console's standard output stream. +/// The property can be set to direct the output to the +/// error stream. +/// +/// +/// NOTE: This appender writes directly to the application's attached console +/// not to the System.Console.Out or System.Console.Error TextWriter. +/// The System.Console.Out and System.Console.Error streams can be +/// programmatically redirected (for example NUnit does this to capture program output). +/// This appender will ignore these redirections because it needs to use Win32 +/// API calls to colorize the output. To respect these redirections the +/// must be used. +/// +/// +/// When configuring the colored console appender, mapping should be +/// specified to map a logging level to a color. For example: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// The Level is the standard log4net logging level and ForeColor and BackColor can be any +/// combination of the following values: +/// +/// Blue +/// Green +/// Red +/// White +/// Yellow +/// Purple +/// Cyan +/// HighIntensity +/// +/// +/// +/// Rick Hobbs +/// Nicko Cadell +public class ColoredConsoleAppender : AppenderSkeleton { /// - /// Appends logging events to the console. + /// The enum of possible color values for use with the color mapping method /// /// /// - /// ColoredConsoleAppender appends log events to the standard output stream - /// or the error output stream using a layout specified by the - /// user. It also allows the color of a specific type of message to be set. - /// - /// - /// By default, all output is written to the console's standard output stream. - /// The property can be set to direct the output to the - /// error stream. - /// - /// - /// NOTE: This appender writes directly to the application's attached console - /// not to the System.Console.Out or System.Console.Error TextWriter. - /// The System.Console.Out and System.Console.Error streams can be - /// programmatically redirected (for example NUnit does this to capture program output). - /// This appender will ignore these redirections because it needs to use Win32 - /// API calls to colorize the output. To respect these redirections the - /// must be used. - /// - /// - /// When configuring the colored console appender, mapping should be - /// specified to map a logging level to a color. For example: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The Level is the standard log4net logging level and ForeColor and BackColor can be any - /// combination of the following values: - /// - /// Blue - /// Green - /// Red - /// White - /// Yellow - /// Purple - /// Cyan - /// HighIntensity - /// + /// The following flags can be combined to form the colors. /// /// - /// Rick Hobbs - /// Nicko Cadell - public class ColoredConsoleAppender : AppenderSkeleton + /// + [Flags] + public enum Colors { /// - /// The enum of possible color values for use with the color mapping method + /// color is blue /// - /// - /// - /// The following flags can be combined to form the colors. - /// - /// - /// - [Flags] - public enum Colors - { - /// - /// color is blue - /// - Blue = 0x0001, - - /// - /// color is green - /// - Green = 0x0002, - - /// - /// color is red - /// - Red = 0x0004, - - /// - /// color is white - /// - White = Blue | Green | Red, - - /// - /// color is yellow - /// - Yellow = Red | Green, - - /// - /// color is purple - /// - Purple = Red | Blue, - - /// - /// color is cyan - /// - Cyan = Green | Blue, - - /// - /// color is intensified - /// - HighIntensity = 0x0008, - } + Blue = 0x0001, /// - /// Initializes a new instance of the class. + /// color is green /// - /// - /// The instance of the class is set up to write - /// to the standard output stream. - /// - public ColoredConsoleAppender() - { - } + Green = 0x0002, /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". + /// color is red /// - /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". - /// - /// - /// - /// Target is the value of the console output stream. - /// This is either "Console.Out" or "Console.Error". - /// - /// - public virtual string Target - { - get => m_writeToErrorStream ? ConsoleError : ConsoleOut; - set => m_writeToErrorStream = StringComparer.OrdinalIgnoreCase.Equals(ConsoleError, value.Trim()); - } + Red = 0x0004, /// - /// Add a mapping of level to color - done by the config file + /// color is white /// - /// The mapping to add - /// - /// - /// Add a mapping to this appender. - /// Each mapping defines the foreground and background colors - /// for a level. - /// - /// - public void AddMapping(LevelColors mapping) - { - m_levelMapping.Add(mapping); - } + White = Blue | Green | Red, /// - /// This method is called by the method. + /// color is yellow /// - /// The event to log. - /// - /// - /// Writes the event to the console. - /// - /// - /// The format of the output will depend on the appender's layout. - /// - /// - [System.Security.SecuritySafeCritical] - [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] - protected override void Append(LoggingEvent loggingEvent) - { - if (m_consoleOutputWriter is not null) - { - IntPtr consoleHandle = GetStdHandle(m_writeToErrorStream ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE); - - // Default to white on black - ushort colorInfo = (ushort)Colors.White; - - // see if there is a specified lookup - if (m_levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) - { - colorInfo = levelColors.CombinedColor; - } - - // Render the event to a string - string strLoggingMessage = RenderLoggingEvent(loggingEvent); - - // get the current console color - to restore later - GetConsoleScreenBufferInfo(consoleHandle, out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); - - // set the console colors - SetConsoleTextAttribute(consoleHandle, colorInfo); - - // Using WriteConsoleW seems to be unreliable. - // If a large buffer is written, say 15,000 chars - // Followed by a larger buffer, say 20,000 chars - // then WriteConsoleW will fail, last error 8 - // 'Not enough storage is available to process this command.' - // - // Although the documentation states that the buffer must - // be less that 64KB (i.e. 32,000 WCHARs) the longest string - // that I can write out a the first call to WriteConsoleW - // is only 30,704 chars. - // - // Unlike the WriteFile API the WriteConsoleW method does not - // seem to be able to partially write out from the input buffer. - // It does have a lpNumberOfCharsWritten parameter, but this is - // either the length of the input buffer if any output was written, - // or 0 when an error occurs. - // - // All results above were observed on Windows XP SP1 running - // .NET runtime 1.1 SP1. - // - // Old call to WriteConsoleW: - // - // WriteConsoleW( - // consoleHandle, - // strLoggingMessage, - // (UInt32)strLoggingMessage.Length, - // out (UInt32)ignoreWrittenCount, - // IntPtr.Zero); - // - // Instead of calling WriteConsoleW we use WriteFile which - // handles large buffers correctly. Because WriteFile does not - // handle the codepage conversion as WriteConsoleW does we - // need to use a System.IO.StreamWriter with the appropriate - // Encoding. The WriteFile calls are wrapped up in the - // System.IO.__ConsoleStream internal class obtained through - // the System.Console.OpenStandardOutput method. - // - // See the ActivateOptions method below for the code that - // retrieves and wraps the stream. - - - // The windows console uses ScrollConsoleScreenBuffer internally to - // scroll the console buffer when the display buffer of the console - // has been used up. ScrollConsoleScreenBuffer fills the area uncovered - // by moving the current content with the background color - // currently specified on the console. This means that it fills the - // whole line in front of the cursor position with the current - // background color. - // This causes an issue when writing out text with a non default - // background color. For example; We write a message with a Blue - // background color and the scrollable area of the console is full. - // When we write the newline at the end of the message the console - // needs to scroll the buffer to make space available for the new line. - // The ScrollConsoleScreenBuffer internals will fill the newly created - // space with the current background color: Blue. - // We then change the console color back to default (White text on a - // Black background). We write some text to the console, the text is - // written correctly in White with a Black background, however the - // remainder of the line still has a Blue background. - // - // This causes a disjointed appearance to the output where the background - // colors change. - // - // This can be remedied by restoring the console colors before causing - // the buffer to scroll, i.e. before writing the last newline. This does - // assume that the rendered message will end with a newline. - // - // Therefore we identify a trailing newline in the message and don't - // write this to the output, then we restore the console color and write - // a newline. Note that we must AutoFlush before we restore the console - // color otherwise we will have no effect. - // - // There will still be a slight artefact for the last line of the message - // will have the background extended to the end of the line, however this - // is unlikely to cause any user issues. - // - // Note that none of the above is visible while the console buffer is scrollable - // within the console window viewport, the effects only arise when the actual - // buffer is full and needs to be scrolled. - - char[] messageCharArray = strLoggingMessage.ToCharArray(); - int arrayLength = messageCharArray.Length; - bool appendNewline = false; - - // Trim off last newline, if it exists - if (arrayLength > 1 && messageCharArray[arrayLength - 2] == '\r' && messageCharArray[arrayLength - 1] == '\n') - { - arrayLength -= 2; - appendNewline = true; - } - - // Write to the output stream - m_consoleOutputWriter.Write(messageCharArray, 0, arrayLength); - - // Restore the console back to its previous color scheme - SetConsoleTextAttribute(consoleHandle, bufferInfo.wAttributes); - - if (appendNewline) - { - // Write the newline, after changing the color scheme - m_consoleOutputWriter.Write(s_windowsNewline, 0, 2); - } - } - } + Yellow = Red | Green, - private static readonly char[] s_windowsNewline = { '\r', '\n' }; + /// + /// color is purple + /// + Purple = Red | Blue, /// - /// This appender requires a to be set. + /// color is cyan /// - protected override bool RequiresLayout => true; + Cyan = Green | Blue, /// - /// Initializes the options for this appender. + /// color is intensified /// - [System.Security.SecuritySafeCritical] - [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] - public override void ActivateOptions() + HighIntensity = 0x0008, + } + + private static readonly char[] windowsNewline = { '\r', '\n' }; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The instance of the class is set up to write + /// to the standard output stream. + /// + public ColoredConsoleAppender() + { } + + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + /// + /// Target is the value of the console output stream. + /// This is either "Console.Out" or "Console.Error". + /// + /// + public virtual string Target + { + get => writeToErrorStream ? ConsoleError : ConsoleOut; + set => writeToErrorStream = StringComparer.OrdinalIgnoreCase.Equals(ConsoleError, value.Trim()); + } + + /// + /// Add a mapping of level to color - done by the config file + /// + /// The mapping to add + /// + /// + /// Add a mapping to this appender. + /// Each mapping defines the foreground and background colors + /// for a level. + /// + /// + public void AddMapping(LevelColors mapping) => levelMapping.Add(mapping); + + /// + /// This method is called by the method. + /// + /// The event to log. + /// + /// + /// Writes the event to the console. + /// + /// + /// The format of the output will depend on the appender's layout. + /// + /// + [System.Security.SecuritySafeCritical] + [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] + protected override void Append(LoggingEvent loggingEvent) + { + if (consoleOutputWriter is not null) { - base.ActivateOptions(); - m_levelMapping.ActivateOptions(); + IntPtr consoleHandle = GetStdHandle(writeToErrorStream ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE); + + // Default to white on black + ushort colorInfo = (ushort)Colors.White; - // Use the Console methods to open a Stream over the console std handle - Stream consoleOutputStream = m_writeToErrorStream ? Console.OpenStandardError() : Console.OpenStandardOutput(); + // see if there is a specified lookup + if (levelMapping.Lookup(loggingEvent.Level) is LevelColors levelColors) + { + colorInfo = levelColors.CombinedColor; + } + + // Render the event to a string + string strLoggingMessage = RenderLoggingEvent(loggingEvent); + + // get the current console color - to restore later + GetConsoleScreenBufferInfo(consoleHandle, out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); + + // set the console colors + SetConsoleTextAttribute(consoleHandle, colorInfo); + + // Using WriteConsoleW seems to be unreliable. + // If a large buffer is written, say 15,000 chars + // Followed by a larger buffer, say 20,000 chars + // then WriteConsoleW will fail, last error 8 + // 'Not enough storage is available to process this command.' + // + // Although the documentation states that the buffer must + // be less that 64KB (i.e. 32,000 WCHARs) the longest string + // that I can write out a the first call to WriteConsoleW + // is only 30,704 chars. + // + // Unlike the WriteFile API the WriteConsoleW method does not + // seem to be able to partially write out from the input buffer. + // It does have a lpNumberOfCharsWritten parameter, but this is + // either the length of the input buffer if any output was written, + // or 0 when an error occurs. + // + // All results above were observed on Windows XP SP1 running + // .NET runtime 1.1 SP1. + // + // Old call to WriteConsoleW: + // + // WriteConsoleW( + // consoleHandle, + // strLoggingMessage, + // (UInt32)strLoggingMessage.Length, + // out (UInt32)ignoreWrittenCount, + // IntPtr.Zero); + // + // Instead of calling WriteConsoleW we use WriteFile which + // handles large buffers correctly. Because WriteFile does not + // handle the codepage conversion as WriteConsoleW does we + // need to use a System.IO.StreamWriter with the appropriate + // Encoding. The WriteFile calls are wrapped up in the + // System.IO.__ConsoleStream internal class obtained through + // the System.Console.OpenStandardOutput method. + // + // See the ActivateOptions method below for the code that + // retrieves and wraps the stream. + + + // The windows console uses ScrollConsoleScreenBuffer internally to + // scroll the console buffer when the display buffer of the console + // has been used up. ScrollConsoleScreenBuffer fills the area uncovered + // by moving the current content with the background color + // currently specified on the console. This means that it fills the + // whole line in front of the cursor position with the current + // background color. + // This causes an issue when writing out text with a non default + // background color. For example; We write a message with a Blue + // background color and the scrollable area of the console is full. + // When we write the newline at the end of the message the console + // needs to scroll the buffer to make space available for the new line. + // The ScrollConsoleScreenBuffer internals will fill the newly created + // space with the current background color: Blue. + // We then change the console color back to default (White text on a + // Black background). We write some text to the console, the text is + // written correctly in White with a Black background, however the + // remainder of the line still has a Blue background. + // + // This causes a disjointed appearance to the output where the background + // colors change. + // + // This can be remedied by restoring the console colors before causing + // the buffer to scroll, i.e. before writing the last newline. This does + // assume that the rendered message will end with a newline. + // + // Therefore we identify a trailing newline in the message and don't + // write this to the output, then we restore the console color and write + // a newline. Note that we must AutoFlush before we restore the console + // color otherwise we will have no effect. + // + // There will still be a slight artefact for the last line of the message + // will have the background extended to the end of the line, however this + // is unlikely to cause any user issues. + // + // Note that none of the above is visible while the console buffer is scrollable + // within the console window viewport, the effects only arise when the actual + // buffer is full and needs to be scrolled. + + char[] messageCharArray = strLoggingMessage.ToCharArray(); + int arrayLength = messageCharArray.Length; + bool appendNewline = false; + + // Trim off last newline, if it exists + if (arrayLength > 1 && messageCharArray[arrayLength - 2] == '\r' && messageCharArray[arrayLength - 1] == '\n') + { + arrayLength -= 2; + appendNewline = true; + } - // Look up the codepage encoding for the console - System.Text.Encoding consoleEncoding = System.Text.Encoding.GetEncoding(GetConsoleOutputCP()); + // Write to the output stream + consoleOutputWriter.Write(messageCharArray, 0, arrayLength); - // Create a writer around the console stream - m_consoleOutputWriter = new StreamWriter(consoleOutputStream, consoleEncoding, 0x100) + // Restore the console back to its previous color scheme + SetConsoleTextAttribute(consoleHandle, bufferInfo.wAttributes); + + if (appendNewline) { - AutoFlush = true - }; - - // SuppressFinalize on m_consoleOutputWriter because all it will do is flush - // and close the file handle. Because we have set AutoFlush the additional flush - // is not required. The console file handle should not be closed, so we don't call - // Dispose, Close or the finalizer. - GC.SuppressFinalize(m_consoleOutputWriter); + // Write the newline, after changing the color scheme + consoleOutputWriter.Write(windowsNewline, 0, 2); + } } + } - /// - /// The to use when writing to the Console - /// standard output stream. - /// - public const string ConsoleOut = "Console.Out"; + /// + /// This appender requires a to be set. + /// + protected override bool RequiresLayout => true; - /// - /// The to use when writing to the Console - /// standard error output stream. - /// - public const string ConsoleError = "Console.Error"; + /// + /// Initializes the options for this appender. + /// + [System.Security.SecuritySafeCritical] + [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)] + public override void ActivateOptions() + { + base.ActivateOptions(); + levelMapping.ActivateOptions(); + + // Use the Console methods to open a Stream over the console std handle + Stream consoleOutputStream = writeToErrorStream ? Console.OpenStandardError() : Console.OpenStandardOutput(); + + // Look up the codepage encoding for the console + Encoding consoleEncoding = EncodingWithoutPreamble.Get(Encoding.GetEncoding(GetConsoleOutputCP())); + + // Create a writer around the console stream + consoleOutputWriter = new StreamWriter(consoleOutputStream, consoleEncoding, 0x100) + { + AutoFlush = true + }; + + // SuppressFinalize on m_consoleOutputWriter because all it will do is flush + // and close the file handle. Because we have set AutoFlush the additional flush + // is not required. The console file handle should not be closed, so we don't call + // Dispose, Close or the finalizer. + GC.SuppressFinalize(consoleOutputWriter); + } + + /// + /// The to use when writing to the Console + /// standard output stream. + /// + public const string ConsoleOut = "Console.Out"; + + /// + /// The to use when writing to the Console + /// standard error output stream. + /// + public const string ConsoleError = "Console.Error"; + + /// + /// Flag to write output to the error stream rather than the standard output stream + /// + private bool writeToErrorStream; + /// + /// Mapping from level object to color value + /// + private readonly LevelMapping levelMapping = new(); + + /// + /// The console output stream writer to write to + /// + /// + /// + /// This writer is not thread safe. + /// + /// + private StreamWriter? consoleOutputWriter; + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern int GetConsoleOutputCP(); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern bool SetConsoleTextAttribute( + IntPtr consoleHandle, + ushort attributes); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern bool GetConsoleScreenBufferInfo( + IntPtr consoleHandle, + out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); + + private const uint STD_OUTPUT_HANDLE = unchecked((uint)-11); + private const uint STD_ERROR_HANDLE = unchecked((uint)-12); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern IntPtr GetStdHandle(uint type); + + [StructLayout(LayoutKind.Sequential)] + private struct COORD + { + public ushort x; + public ushort y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SMALL_RECT + { + public ushort Left; + public ushort Top; + public ushort Right; + public ushort Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct CONSOLE_SCREEN_BUFFER_INFO + { + public COORD dwSize; + public COORD dwCursorPosition; + public ushort wAttributes; + public SMALL_RECT srWindow; + public COORD dwMaximumWindowSize; + } + + /// + /// A class to act as a mapping between the level that a logging call is made at and + /// the color it should be displayed as. + /// + /// + /// + /// Defines the mapping between a level and the color it should be displayed in. + /// + /// + public class LevelColors : LevelMappingEntry + { /// - /// Flag to write output to the error stream rather than the standard output stream + /// The mapped foreground color for the specified level /// - private bool m_writeToErrorStream; + public Colors ForeColor { get; set; } /// - /// Mapping from level object to color value + /// The mapped background color for the specified level /// - private readonly LevelMapping m_levelMapping = new(); + public Colors BackColor { get; set; } /// - /// The console output stream writer to write to + /// Initialize the options for the object /// /// /// - /// This writer is not thread safe. + /// Combine the and together. /// /// - private StreamWriter? m_consoleOutputWriter; - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern int GetConsoleOutputCP(); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern bool SetConsoleTextAttribute( - IntPtr consoleHandle, - ushort attributes); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern bool GetConsoleScreenBufferInfo( - IntPtr consoleHandle, - out CONSOLE_SCREEN_BUFFER_INFO bufferInfo); - - private const UInt32 STD_OUTPUT_HANDLE = unchecked((UInt32)(-11)); - private const UInt32 STD_ERROR_HANDLE = unchecked((UInt32)(-12)); - - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - private static extern IntPtr GetStdHandle( - UInt32 type); - - [StructLayout(LayoutKind.Sequential)] - private struct COORD - { - public UInt16 x; - public UInt16 y; - } - - [StructLayout(LayoutKind.Sequential)] - private struct SMALL_RECT - { - public UInt16 Left; - public UInt16 Top; - public UInt16 Right; - public UInt16 Bottom; - } - - [StructLayout(LayoutKind.Sequential)] - private struct CONSOLE_SCREEN_BUFFER_INFO + public override void ActivateOptions() { - public COORD dwSize; - public COORD dwCursorPosition; - public ushort wAttributes; - public SMALL_RECT srWindow; - public COORD dwMaximumWindowSize; + base.ActivateOptions(); + CombinedColor = (ushort)((int)ForeColor + (((int)BackColor) << 4)); } /// - /// A class to act as a mapping between the level that a logging call is made at and - /// the color it should be displayed as. + /// The combined and suitable for + /// setting the console color. /// - /// - /// - /// Defines the mapping between a level and the color it should be displayed in. - /// - /// - public class LevelColors : LevelMappingEntry - { - /// - /// The mapped foreground color for the specified level - /// - public Colors ForeColor { get; set; } - - /// - /// The mapped background color for the specified level - /// - public Colors BackColor { get; set; } - - /// - /// Initialize the options for the object - /// - /// - /// - /// Combine the and together. - /// - /// - public override void ActivateOptions() - { - base.ActivateOptions(); - CombinedColor = (ushort)((int)ForeColor + (((int)BackColor) << 4)); - } - - /// - /// The combined and suitable for - /// setting the console color. - /// - internal ushort CombinedColor { get; private set; } - } + internal ushort CombinedColor { get; private set; } } } \ No newline at end of file diff --git a/src/log4net/Util/EncodingWithoutPreamble.cs b/src/log4net/Util/EncodingWithoutPreamble.cs new file mode 100644 index 00000000..a80cc8fb --- /dev/null +++ b/src/log4net/Util/EncodingWithoutPreamble.cs @@ -0,0 +1,121 @@ +#region Apache License +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +using System.Text; + +namespace log4net.Util; + +/// +/// Wrapper for an +/// +/// acts like the wrapped encoding, but without a preamble +internal sealed class EncodingWithoutPreamble : Encoding +{ + private readonly Encoding wrapped; + + /// + private EncodingWithoutPreamble(Encoding wrapped) => this.wrapped = wrapped.EnsureNotNull(); + + /// + /// wraps the in case it has a preamble + /// + /// Encoding to check + /// encoding without preamble + internal static Encoding Get(Encoding encoding) + => encoding.EnsureNotNull().GetPreamble()?.Length > 0 + ? new EncodingWithoutPreamble(encoding) + : encoding; + + /// + public override string BodyName => wrapped.BodyName; + + /// + public override int CodePage => wrapped.CodePage; + + /// + public override string EncodingName => wrapped.EncodingName; + + /// + public override string HeaderName => wrapped.HeaderName; + + /// + public override bool IsBrowserDisplay => wrapped.IsBrowserDisplay; + + /// + public override bool IsBrowserSave => wrapped.IsBrowserSave; + + /// + public override bool IsMailNewsDisplay => wrapped.IsMailNewsDisplay; + + /// + public override bool IsMailNewsSave => wrapped.IsMailNewsSave; + + /// + public override bool IsSingleByte => wrapped.IsSingleByte; + + /// + public override string WebName => wrapped.WebName; + + /// + public override int WindowsCodePage => wrapped.WindowsCodePage; + + + /// + public override object Clone() => new EncodingWithoutPreamble(wrapped.Clone().EnsureIs()); + + /// + public override int GetByteCount(char[] chars, int index, int count) + => wrapped.GetByteCount(chars, index, count); + + /// + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + => wrapped.GetBytes(chars, charIndex, charCount, bytes, byteIndex); + + /// + public override int GetCharCount(byte[] bytes, int index, int count) + => wrapped.GetCharCount(bytes, index, count); + + /// + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + => wrapped.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + + /// + public override int GetMaxByteCount(int charCount) => wrapped.GetMaxByteCount(charCount); + + /// + public override int GetMaxCharCount(int byteCount) => wrapped.GetMaxCharCount(byteCount); + + /// + public override Decoder GetDecoder() => wrapped.GetDecoder(); + + /// + public override Encoder GetEncoder() => wrapped.GetEncoder(); + + /// + public override bool IsAlwaysNormalized(NormalizationForm form) => wrapped.IsAlwaysNormalized(form); + + /// + public override bool Equals(object value) => wrapped.Equals(value); + + /// + public override int GetHashCode() => wrapped.GetHashCode(); + + /// + public override string ToString() => $"{wrapped}-WithoutPreamble"; +} \ No newline at end of file