File: System\Diagnostics\TextWriterTraceListener.cs
Web Access
Project: src\src\libraries\System.Diagnostics.TextWriterTraceListener\src\System.Diagnostics.TextWriterTraceListener.csproj (System.Diagnostics.TextWriterTraceListener)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO;
using System.Text;
 
namespace System.Diagnostics
{
    /// <devdoc>
    ///    <para>Directs tracing or debugging output to
    ///       a <see cref='System.IO.TextWriter'/> or to a <see cref='System.IO.Stream'/>,
    ///       such as <see cref='System.Console.Out'/> or <see cref='System.IO.FileStream'/>.</para>
    /// </devdoc>
    public class TextWriterTraceListener : TraceListener
    {
        internal TextWriter? _writer;
        private string? _fileName;
 
        /// <devdoc>
        /// <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class with
        /// <see cref='System.IO.TextWriter'/>
        /// as the output recipient.</para>
        /// </devdoc>
        public TextWriterTraceListener()
        {
        }
 
        /// <devdoc>
        /// <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class, using the
        ///    stream as the recipient of the debugging and tracing output.</para>
        /// </devdoc>
        public TextWriterTraceListener(Stream stream)
            : this(stream, string.Empty)
        {
        }
 
        /// <devdoc>
        /// <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class with the
        ///    specified name and using the stream as the recipient of the debugging and tracing output.</para>
        /// </devdoc>
        public TextWriterTraceListener(Stream stream, string? name)
            : base(name)
        {
            ArgumentNullException.ThrowIfNull(stream);
 
            _writer = new StreamWriter(stream);
        }
 
        /// <devdoc>
        /// <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class using the
        ///    specified writer as recipient of the tracing or debugging output.</para>
        /// </devdoc>
        public TextWriterTraceListener(TextWriter writer)
            : this(writer, string.Empty)
        {
        }
 
        /// <devdoc>
        /// <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class with the
        ///    specified name and using the specified writer as recipient of the tracing or
        ///    debugging
        ///    output.</para>
        /// </devdoc>
        public TextWriterTraceListener(TextWriter writer, string? name)
            : base(name)
        {
            ArgumentNullException.ThrowIfNull(writer);
 
            _writer = writer;
        }
 
        /// <devdoc>
        ///    <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class with the
        ///    specified file name.</para>
        /// </devdoc>
        public TextWriterTraceListener(string? fileName)
        {
            _fileName = fileName;
        }
 
        /// <devdoc>
        ///    <para>Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener'/> class with the
        ///    specified name and the specified file name.</para>
        /// </devdoc>
        public TextWriterTraceListener(string? fileName, string? name)
            : base(name)
        {
            _fileName = fileName;
        }
 
        /// <devdoc>
        ///    <para> Indicates the text writer that receives the tracing
        ///       or debugging output.</para>
        /// </devdoc>
        public TextWriter? Writer
        {
            get
            {
                EnsureWriter();
                return _writer;
            }
 
            set
            {
                _writer = value;
            }
        }
 
        /// <devdoc>
        /// <para>Closes the <see cref='System.Diagnostics.TextWriterTraceListener.Writer'/> so that it no longer
        ///    receives tracing or debugging output.</para>
        /// </devdoc>
        public override void Close()
        {
            if (_writer != null)
            {
                try
                {
                    _writer.Close();
                }
                catch (ObjectDisposedException) { }
                _writer = null;
            }
 
            // We need to set the _fileName to null so that we stop tracing output, if we don't set it
            // EnsureWriter will create the stream writer again if someone writes or traces output after closing.
            _fileName = null;
        }
 
        /// <internalonly/>
        /// <devdoc>
        /// </devdoc>
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && _writer != null)
                {
                    _writer.Dispose();
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }
 
        /// <devdoc>
        /// <para>Flushes the output buffer for the <see cref='System.Diagnostics.TextWriterTraceListener.Writer'/>.</para>
        /// </devdoc>
        public override void Flush()
        {
            EnsureWriter();
            try
            {
                _writer?.Flush();
            }
            catch (ObjectDisposedException) { }
        }
 
        /// <devdoc>
        ///    <para>Writes a message
        ///       to this instance's <see cref='System.Diagnostics.TextWriterTraceListener.Writer'/>.</para>
        /// </devdoc>
        public override void Write(string? message)
        {
            EnsureWriter();
            if (_writer != null)
            {
                if (NeedIndent) WriteIndent();
                try
                {
                    _writer.Write(message);
                }
                catch (ObjectDisposedException) { }
            }
        }
 
        /// <devdoc>
        ///    <para>Writes a message
        ///       to this instance's <see cref='System.Diagnostics.TextWriterTraceListener.Writer'/> followed by a line terminator. The
        ///       default line terminator is a carriage return followed by a line feed (\r\n).</para>
        /// </devdoc>
        public override void WriteLine(string? message)
        {
            EnsureWriter();
            if (_writer != null)
            {
                if (NeedIndent) WriteIndent();
                try
                {
                    _writer.WriteLine(message);
                    NeedIndent = true;
                }
                catch (ObjectDisposedException) { }
            }
        }
 
        internal void EnsureWriter()
        {
            if (_writer == null)
            {
                InitializeWriter();
            }
 
            void InitializeWriter()
            {
                bool success = false;
 
                if (_fileName == null)
                    return;
 
                // StreamWriter by default uses UTF8Encoding which will throw on invalid encoding errors.
                // This can cause the internal StreamWriter's state to be irrecoverable. It is bad for tracing
                // APIs to throw on encoding errors. Instead, we should provide a "?" replacement fallback
                // encoding to substitute illegal chars. For ex, In case of high surrogate character
                // D800-DBFF without a following low surrogate character DC00-DFFF
                // NOTE: We also need to use an encoding that does't emit BOM which is StreamWriter's default
                var noBOMwithFallback = (UTF8Encoding)new UTF8Encoding(false).Clone();
                noBOMwithFallback.EncoderFallback = EncoderFallback.ReplacementFallback;
                noBOMwithFallback.DecoderFallback = DecoderFallback.ReplacementFallback;
 
                // To support multiple appdomains/instances tracing to the same file,
                // we will try to open the given file for append but if we encounter
                // IO errors, we will prefix the file name with a unique GUID value
                // and try one more time
                string fullPath = Path.GetFullPath(_fileName);
                string dirPath = Path.GetDirectoryName(fullPath)!;
                string fileNameOnly = Path.GetFileName(fullPath);
 
                for (int i = 0; i < 2; i++)
                {
                    try
                    {
                        _writer = new StreamWriter(fullPath, true, noBOMwithFallback, 4096);
                        success = true;
                        break;
                    }
                    catch (IOException)
                    {
                        fileNameOnly = $"{Guid.NewGuid()}{fileNameOnly}";
                        fullPath = Path.Combine(dirPath, fileNameOnly);
                        continue;
                    }
                    catch (UnauthorizedAccessException)
                    {
                        //ERROR_ACCESS_DENIED, mostly ACL issues
                        break;
                    }
                    catch (Exception)
                    {
                        break;
                    }
                }
 
                if (!success)
                {
                    // Disable tracing to this listener. Every Write will be nop.
                    // We need to think of a central way to deal with the listener
                    // init errors in the future. The default should be that we eat
                    // up any errors from listener and optionally notify the user
                    _fileName = null;
                }
            }
        }
 
        internal bool IsEnabled(TraceOptions opts) => (opts & TraceOutputOptions) != 0;
    }
}