Skip to content

Commit

Permalink
Add console file logging (#3358)
Browse files Browse the repository at this point in the history
Add console file logging

Added "logopen" and "logclose" commands to control console file logging.

Added the IConsoleFileLoggingService and implementation to control the console file logging.

Issue: #3095

Add internal diagnostic logging to a file also.

Added the IDiagnosticLoggingService and implementation to control internal diagnostic logging.

Move Tracer.cs to Microsoft.Diagnostics.DebugService.Implementation.

Add tests for logging commands
  • Loading branch information
mikem8361 committed Sep 9, 2022
1 parent ea1acd6 commit a529794
Show file tree
Hide file tree
Showing 15 changed files with 597 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Diagnostics.DebugServices;
using System;
using System.Diagnostics;
using System.IO;
using System.Security;

namespace Microsoft.Diagnostics.ExtensionCommands
{
public class DiagnosticLoggingService : IDiagnosticLoggingService
{
private const string ListenerName = "SOS.LoggingListener";
private IConsoleService _consoleService;
private IConsoleFileLoggingService _fileLoggingService;
private StreamWriter _writer;

public static DiagnosticLoggingService Instance { get; } = new DiagnosticLoggingService();

private DiagnosticLoggingService()
{
}

#region IDiagnosticLoggingService

/// <summary>
/// Returns true if logging to console or file
/// </summary>
public bool IsEnabled => Trace.Listeners[ListenerName] is not null;

/// <summary>
/// The file path if logging to file.
/// </summary>
public string FilePath => (_writer?.BaseStream as FileStream)?.Name;

/// <summary>
/// Enable diagnostics logging.
/// </summary>
/// <param name="filePath">log file path or null if log to console</param>
/// <remarks>see File.Open for possible exceptions thrown</remarks>
public void Enable(string filePath)
{
if (filePath is not null)
{
FileStream stream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
CloseLogging();
_writer = new StreamWriter(stream) {
AutoFlush = true
};
_fileLoggingService?.AddStream(stream);
}
if (Trace.Listeners[ListenerName] is null)
{
Trace.Listeners.Add(new LoggingListener(this));
Trace.AutoFlush = true;
}
}

/// <summary>
/// Disable diagnostics logging (close if logging to file).
/// </summary>
public void Disable()
{
CloseLogging();
Trace.Listeners.Remove(ListenerName);
}

#endregion

/// <summary>
/// Initializes the diagnostic logging service. Reads the DOTNET_ENABLED_SOS_LOGGING
/// environment variable to log to console or file.
/// </summary>
/// <param name="logfile"></param>
public static void Initialize(string logfile = null)
{
try
{
if (string.IsNullOrWhiteSpace(logfile))
{
logfile = Environment.GetEnvironmentVariable("DOTNET_ENABLED_SOS_LOGGING");
}
if (!string.IsNullOrWhiteSpace(logfile))
{
Instance.Enable(logfile == "1" ? null : logfile);
}
}
catch (Exception ex) when ( ex is IOException || ex is NotSupportedException || ex is SecurityException || ex is UnauthorizedAccessException)
{
}
}

/// <summary>
/// Sets the console service and the console file logging control service.
/// </summary>
/// <param name="consoleService">This is used for to log to the console</param>
/// <param name="fileLoggingService">This is used to hook the command console output to write the diagnostic log file.</param>
public void SetConsole(IConsoleService consoleService, IConsoleFileLoggingService fileLoggingService = null)
{
_consoleService = consoleService;
_fileLoggingService = fileLoggingService;
}

private void CloseLogging()
{
if (_writer is not null)
{
_fileLoggingService?.RemoveStream(_writer.BaseStream);
_writer.Flush();
_writer.Close();
_writer = null;
}
}

class LoggingListener : TraceListener
{
private readonly DiagnosticLoggingService _diagnosticLoggingService;

internal LoggingListener(DiagnosticLoggingService diagnosticLoggingService)
: base(ListenerName)
{
_diagnosticLoggingService = diagnosticLoggingService;
}

public override void Close()
{
_diagnosticLoggingService.CloseLogging();
base.Close();
}

public override void Write(string message)
{
if (_diagnosticLoggingService._writer is not null)
{
try
{
_diagnosticLoggingService._writer.Write(message);
return;
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
_diagnosticLoggingService._consoleService?.Write(message);
}

public override void WriteLine(string message)
{
if (_diagnosticLoggingService._writer is not null)
{
try
{
_diagnosticLoggingService._writer.WriteLine(message);
return;
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
_diagnosticLoggingService._consoleService?.WriteLine(message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace Microsoft.Diagnostics.DebugServices.Implementation
{
/// <summary>
/// Log to file console service wrapper
/// </summary>
public class FileLoggingConsoleService : IConsoleService, IConsoleFileLoggingService, IDisposable
{
private readonly IConsoleService _consoleService;
private readonly List<StreamWriter> _writers;
private FileStream _consoleStream;

public FileLoggingConsoleService(IConsoleService consoleService)
{
_consoleService = consoleService;
_writers = new List<StreamWriter>();
}

public void Dispose() => Disable();

#region IConsoleFileLoggingService

/// <summary>
/// The log file path if enabled, otherwise null.
/// </summary>
public string FilePath => _consoleStream?.Name;

/// <summary>
/// Enable console file logging.
/// </summary>
/// <param name="filePath">log file path</param>
/// <remarks>see File.Open for more exceptions</remarks>
public void Enable(string filePath)
{
FileStream consoleStream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
Disable();
AddStream(consoleStream);
_consoleStream = consoleStream;
}

/// <summary>
/// Disable/close console file logging
/// </summary>
public void Disable()
{
if (_consoleStream is not null)
{
RemoveStream(_consoleStream);
_consoleStream.Close();
_consoleStream = null;
}
}

/// <summary>
/// Add to the list of file streams to write the console output.
/// </summary>
/// <param name="stream">Stream to add. Lifetime managed by caller.</param>
public void AddStream(Stream stream)
{
Debug.Assert(stream is not null);
_writers.Add(new StreamWriter(stream) {
AutoFlush = true
});
}

/// <summary>
/// Remove the specified file stream from the writers.
/// </summary>
/// <param name="stream">Stream passed to add. Stream not closed or disposed.</param>
public void RemoveStream(Stream stream)
{
if (stream is not null)
{
foreach (StreamWriter writer in _writers)
{
if (writer.BaseStream == stream)
{
_writers.Remove(writer);
break;
}
}
}
}

#endregion

#region IConsoleService

public void Write(string text)
{
_consoleService.Write(text);
foreach (StreamWriter writer in _writers)
{
try
{
writer.Write(text);
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
}

public void WriteWarning(string text)
{
_consoleService.WriteWarning(text);
foreach (StreamWriter writer in _writers)
{
try
{
writer.Write(text);
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
}

public void WriteError(string text)
{
_consoleService.WriteError(text);
foreach (StreamWriter writer in _writers)
{
try
{
writer.Write(text);
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
}

public bool SupportsDml => _consoleService.SupportsDml;

public void WriteDml(string text)
{
_consoleService.WriteDml(text);
foreach (StreamWriter writer in _writers)
{
try
{
// TODO: unwrap the DML?
writer.Write(text);
}
catch (Exception ex) when (ex is IOException || ex is ObjectDisposedException || ex is NotSupportedException)
{
}
}
}

public CancellationToken CancellationToken
{
get { return _consoleService.CancellationToken; }
set { _consoleService.CancellationToken = value; }
}

public int WindowWidth => _consoleService.WindowWidth;

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public SymbolService(IHost host)
{
_host = host;
OnChangeEvent = new ServiceEvent();
// dbgeng's console can not handle the async logging (Tracer output on another thread than the main one).
Tracer.Enable = host.HostType != HostType.DbgEng;
}

#region ISymbolService
Expand Down
Loading

0 comments on commit a529794

Please sign in to comment.