| File: System\Diagnostics\TextWriterTraceListener.cs | Web Access |
| Project: src\runtime\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 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; } }