From a529794a9092019b29a60ac41c455b5c67c465e3 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Fri, 9 Sep 2022 15:25:25 -0700 Subject: [PATCH] Add console file logging (#3358) 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: https://github.com/dotnet/diagnostics/issues/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 --- .../DiagnosticLoggingService.cs | 166 +++++++++++++++++ .../FileLoggingConsoleService.cs | 171 ++++++++++++++++++ .../SymbolService.cs | 2 + .../Tracer.cs | 58 +++++- .../IConsoleFileLoggingService.cs | 43 +++++ .../IDiagnosticLoggingService.cs | 34 ++++ .../Host/ConsoleLoggingCommand.cs | 46 +++++ .../Host/LoggingCommand.cs | 62 ++----- .../ConsoleService.cs | 20 +- .../TestConfiguration.cs | 25 +-- .../TestRunner.cs | 2 +- src/SOS/SOS.Extensions/HostServices.cs | 8 +- src/SOS/SOS.UnitTests/SOSRunner.cs | 3 + .../Scripts/OtherCommands.script | 12 +- src/Tools/dotnet-dump/Analyzer.cs | 41 +++-- 15 files changed, 597 insertions(+), 96 deletions(-) create mode 100644 src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs create mode 100644 src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs rename src/{Microsoft.Diagnostics.DebugServices => Microsoft.Diagnostics.DebugServices.Implementation}/Tracer.cs (57%) create mode 100644 src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs create mode 100644 src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs create mode 100644 src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs new file mode 100644 index 000000000..94ee4a312 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs @@ -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 + + /// + /// Returns true if logging to console or file + /// + public bool IsEnabled => Trace.Listeners[ListenerName] is not null; + + /// + /// The file path if logging to file. + /// + public string FilePath => (_writer?.BaseStream as FileStream)?.Name; + + /// + /// Enable diagnostics logging. + /// + /// log file path or null if log to console + /// see File.Open for possible exceptions thrown + 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; + } + } + + /// + /// Disable diagnostics logging (close if logging to file). + /// + public void Disable() + { + CloseLogging(); + Trace.Listeners.Remove(ListenerName); + } + + #endregion + + /// + /// Initializes the diagnostic logging service. Reads the DOTNET_ENABLED_SOS_LOGGING + /// environment variable to log to console or file. + /// + /// + 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) + { + } + } + + /// + /// Sets the console service and the console file logging control service. + /// + /// This is used for to log to the console + /// This is used to hook the command console output to write the diagnostic log file. + 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); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs new file mode 100644 index 000000000..ea041eda9 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs @@ -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 +{ + /// + /// Log to file console service wrapper + /// + public class FileLoggingConsoleService : IConsoleService, IConsoleFileLoggingService, IDisposable + { + private readonly IConsoleService _consoleService; + private readonly List _writers; + private FileStream _consoleStream; + + public FileLoggingConsoleService(IConsoleService consoleService) + { + _consoleService = consoleService; + _writers = new List(); + } + + public void Dispose() => Disable(); + + #region IConsoleFileLoggingService + + /// + /// The log file path if enabled, otherwise null. + /// + public string FilePath => _consoleStream?.Name; + + /// + /// Enable console file logging. + /// + /// log file path + /// see File.Open for more exceptions + public void Enable(string filePath) + { + FileStream consoleStream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + Disable(); + AddStream(consoleStream); + _consoleStream = consoleStream; + } + + /// + /// Disable/close console file logging + /// + public void Disable() + { + if (_consoleStream is not null) + { + RemoveStream(_consoleStream); + _consoleStream.Close(); + _consoleStream = null; + } + } + + /// + /// Add to the list of file streams to write the console output. + /// + /// Stream to add. Lifetime managed by caller. + public void AddStream(Stream stream) + { + Debug.Assert(stream is not null); + _writers.Add(new StreamWriter(stream) { + AutoFlush = true + }); + } + + /// + /// Remove the specified file stream from the writers. + /// + /// Stream passed to add. Stream not closed or disposed. + 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 + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs index 3f62861b2..58b453f1b 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs @@ -41,6 +41,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { _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 diff --git a/src/Microsoft.Diagnostics.DebugServices/Tracer.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Tracer.cs similarity index 57% rename from src/Microsoft.Diagnostics.DebugServices/Tracer.cs rename to src/Microsoft.Diagnostics.DebugServices.Implementation/Tracer.cs index 516d495c0..6a7d59377 100644 --- a/src/Microsoft.Diagnostics.DebugServices/Tracer.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Tracer.cs @@ -2,16 +2,19 @@ // 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.SymbolStore; using System.Diagnostics; -namespace Microsoft.Diagnostics.DebugServices +namespace Microsoft.Diagnostics.DebugServices.Implementation { /// /// Simple trace/logging support. /// - public sealed class Tracer : Microsoft.SymbolStore.ITracer + public sealed class Tracer : ITracer { - public static Microsoft.SymbolStore.ITracer Instance { get; } = new Tracer(); + public static bool Enable { get; set; } + + public static ITracer Instance { get; } = Enable ? new Tracer() : new NullTracer(); private Tracer() { @@ -45,7 +48,7 @@ namespace Microsoft.Diagnostics.DebugServices Trace.TraceWarning(message); Trace.Flush(); } - + public void Warning(string format, params object[] arguments) { Trace.TraceWarning(format, arguments); @@ -73,5 +76,52 @@ namespace Microsoft.Diagnostics.DebugServices { Information(format, arguments); } + + sealed class NullTracer : ITracer + { + internal NullTracer() + { + } + + public void WriteLine(string message) + { + } + + public void WriteLine(string format, params object[] arguments) + { + } + + public void Information(string message) + { + } + + public void Information(string format, params object[] arguments) + { + } + + public void Warning(string message) + { + } + + public void Warning(string format, params object[] arguments) + { + } + + public void Error(string message) + { + } + + public void Error(string format, params object[] arguments) + { + } + + public void Verbose(string message) + { + } + + public void Verbose(string format, params object[] arguments) + { + } + } } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs b/src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs new file mode 100644 index 000000000..58592867e --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs @@ -0,0 +1,43 @@ +// 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.IO; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Console file logging control service + /// + public interface IConsoleFileLoggingService + { + /// + /// The log file path if enabled, otherwise null. + /// + string FilePath { get; } + + /// + /// Enable console file logging. + /// + /// log file path + /// see File.Open for possible exceptions thrown + void Enable(string filePath); + + /// + /// Disable/close console file logging. + /// + void Disable(); + + /// + /// Add to the list of file streams to write the console output. + /// + /// Stream to add. Lifetime managed by caller. + void AddStream(Stream stream); + + /// + /// Remove the specified file stream from the writers. + /// + /// Stream passed to add. Stream not closed or disposed. + void RemoveStream(Stream stream); + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs b/src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs new file mode 100644 index 000000000..de2ed45f1 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs @@ -0,0 +1,34 @@ +// 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. + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Service to control the internal diagnostic (Trace) logging. + /// + public interface IDiagnosticLoggingService + { + /// + /// Returns true if logging to console or file + /// + bool IsEnabled { get; } + + /// + /// The file path if logging to file. + /// + string FilePath { get; } + + /// + /// Enable diagnostics logging. + /// + /// log file path or null if log to console + /// see File.Open for possible exceptions thrown + void Enable(string filePath); + + /// + /// Disable diagnostics logging (close if logging to file). + /// + void Disable(); + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs new file mode 100644 index 000000000..18eb2cb87 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "logopen", Help = "Enable console file logging", Flags = CommandFlags.Global)] + [Command(Name = "logclose", DefaultOptions = "--disable", Help = "Disable console file logging", Flags = CommandFlags.Global)] + public class ConsoleLoggingCommand : CommandBase + { + public IConsoleFileLoggingService FileLoggingService { get; set; } + + [Argument(Name = "path", Help = "Log file path.")] + public string FilePath { get; set; } + + [Option(Name = "--disable", Help = "Disable console file logging.")] + public bool Disable { get; set; } + + public override void Invoke() + { + if (FileLoggingService is null) + { + throw new DiagnosticsException("Console logging is not supported"); + } + if (Disable) + { + FileLoggingService.Disable(); + } + else if (!string.IsNullOrWhiteSpace(FilePath)) + { + FileLoggingService.Enable(FilePath); + } + string filePath = FileLoggingService.FilePath; + if (filePath is not null) + { + WriteLine($"Console is logging to {filePath}"); + } + else + { + WriteLine("Console logging is disabled"); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs index 974a79681..11f969b8d 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs @@ -5,68 +5,44 @@ using Microsoft.Diagnostics.DebugServices; using System; using System.Diagnostics; +using System.IO; +using System.Security; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "logging", Help = "Enable/disable internal logging", Flags = CommandFlags.Global)] + [Command(Name = "logging", Help = "Enable/disable internal diagnostic logging", Flags = CommandFlags.Global)] public class LoggingCommand : CommandBase { - [Option(Name = "enable", Help = "Enable internal logging.")] + public IDiagnosticLoggingService DiagnosticLoggingService { get; set; } + + [Argument(Name = "path", Help = "Log file path.")] + public string FilePath { get; set; } + + [Option(Name = "--enable", Aliases = new string[] { "enable", "-e" }, Help = "Enable internal logging.")] public bool Enable { get; set; } - [Option(Name = "disable", Help = "Disable internal logging.")] + [Option(Name = "--disable", Aliases = new string[] { "disable", "-d"}, Help = "Disable internal logging.")] public bool Disable { get; set; } - private const string ListenerName = "Analyze.LoggingListener"; - public override void Invoke() { - if (Enable) { - EnableLogging(); - } - else if (Disable) { - DisableLogging(); - } - WriteLine("Logging is {0}", Trace.Listeners[ListenerName] != null ? "enabled" : "disabled"); - } - - public static void Initialize() - { - if (Environment.GetEnvironmentVariable("DOTNET_ENABLED_SOS_LOGGING") == "1") - { - EnableLogging(); - } - } - - public static void EnableLogging() - { - if (Trace.Listeners[ListenerName] == null) + if (DiagnosticLoggingService is null) { - Trace.Listeners.Add(new LoggingListener()); - Trace.AutoFlush = true; + throw new DiagnosticsException("Diagnostic logging is not supported"); } - } - - public static void DisableLogging() - { - Trace.Listeners.Remove(ListenerName); - } - - class LoggingListener : TraceListener - { - internal LoggingListener() - : base(ListenerName) + if (Disable) { + DiagnosticLoggingService.Disable(); } - - public override void Write(string message) + else if (Enable || !string.IsNullOrWhiteSpace(FilePath)) { - System.Console.Write(message); + DiagnosticLoggingService.Enable(FilePath); } + WriteLine("Logging is {0}", DiagnosticLoggingService.IsEnabled ? "enabled" : "disabled"); - public override void WriteLine(string message) + if (!string.IsNullOrWhiteSpace(DiagnosticLoggingService.FilePath)) { - System.Console.WriteLine(message); + WriteLine(DiagnosticLoggingService.FilePath); } } } diff --git a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs index 27bd1b422..c14fba57b 100644 --- a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs +++ b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs @@ -73,7 +73,7 @@ namespace Microsoft.Diagnostics.Repl /// Start input processing and command dispatching /// /// Called to dispatch a command on ENTER - public void Start(Action dispatchCommand) + public void Start(Action dispatchCommand) { m_lastCommandLine = null; m_interactiveConsole = !Console.IsInputRedirected; @@ -159,14 +159,6 @@ namespace Microsoft.Diagnostics.Repl RefreshLine(); } - /// - /// Writes a message with a new line to console. - /// - public void WriteLine(string format, params object[] parameters) - { - WriteLine(OutputType.Normal, format, parameters); - } - /// /// Writes a message with a new line to console. /// @@ -338,7 +330,7 @@ namespace Microsoft.Diagnostics.Repl m_refreshingLine = false; } - private void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action dispatchCommand) + private void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action dispatchCommand) { int activeLineLen = m_activeLine.Length; @@ -455,7 +447,7 @@ namespace Microsoft.Diagnostics.Repl } } - private bool Dispatch(string newCommand, Action dispatchCommand) + private bool Dispatch(string newCommand, Action dispatchCommand) { bool result = true; CommandStarting(); @@ -469,8 +461,7 @@ namespace Microsoft.Diagnostics.Repl } try { - WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand); - dispatchCommand(newCommand, m_interruptExecutingCommand.Token); + dispatchCommand(m_prompt, newCommand, m_interruptExecutingCommand.Token); m_lastCommandLine = newCommand; } catch (OperationCanceledException) @@ -478,8 +469,9 @@ namespace Microsoft.Diagnostics.Repl // ctrl-c interrupted the command m_lastCommandLine = null; } - catch (Exception ex) when (!(ex is NullReferenceException || ex is ArgumentNullException || ex is ArgumentException)) + catch (Exception ex) { + // Most exceptions should not excape the command dispatch, but just in case WriteLine(OutputType.Error, "ERROR: {0}", ex.Message); Trace.TraceError(ex.ToString()); m_lastCommandLine = null; diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs b/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs index 5aa681063..95b41a190 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs @@ -466,20 +466,23 @@ namespace Microsoft.Diagnostics.TestHelpers return sb.ToString(); } - internal string GetLogSuffix() + public string LogSuffix { - string version = RuntimeFrameworkVersion; - - // The log name can't contain wild cards, which are used in some testing scenarios. - // TODO: The better solution would be to sanitize the file name properly, in case - // there's a key being used that contains a character that is not a valid file - // name charater. - if (!string.IsNullOrEmpty(version) && version.Contains('*')) + get { - version = _truncatedRuntimeFrameworkVersion; - } + string version = RuntimeFrameworkVersion; - return GetStringViewWithVersion(version); + // The log name can't contain wild cards, which are used in some testing scenarios. + // TODO: The better solution would be to sanitize the file name properly, in case + // there's a key being used that contains a character that is not a valid file + // name charater. + if (!string.IsNullOrEmpty(version) && version.Contains('*')) + { + version = _truncatedRuntimeFrameworkVersion; + } + + return GetStringViewWithVersion(version); + } } public IReadOnlyDictionary AllSettings diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs b/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs index bc8e2f7d5..2176a6654 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs @@ -139,7 +139,7 @@ namespace Microsoft.Diagnostics.TestHelpers ConsoleTestOutputHelper consoleLogger = null; if (!string.IsNullOrEmpty(config.LogDirPath)) { - string logFileName = $"{ testName }.{ config.GetLogSuffix() }.log"; + string logFileName = $"{testName}.{config.LogSuffix}.log"; string logPath = Path.Combine(config.LogDirPath, logFileName); fileLogger = new FileTestOutputHelper(logPath, FileMode.Append); } diff --git a/src/SOS/SOS.Extensions/HostServices.cs b/src/SOS/SOS.Extensions/HostServices.cs index f83d94b6d..b44ebebe3 100644 --- a/src/SOS/SOS.Extensions/HostServices.cs +++ b/src/SOS/SOS.Extensions/HostServices.cs @@ -54,7 +54,7 @@ namespace SOS.Extensions if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) { AssemblyResolver.Enable(); } - LoggingCommand.Initialize(); + DiagnosticLoggingService.Initialize(); } /// @@ -107,6 +107,7 @@ namespace SOS.Extensions private HostServices() { _serviceProvider = new ServiceProvider(); + _serviceProvider.AddService(DiagnosticLoggingService.Instance); _symbolService = new SymbolService(this); _symbolService.DefaultTimeout = DefaultTimeout; _symbolService.DefaultRetryCount = DefaultRetryCount; @@ -217,7 +218,10 @@ namespace SOS.Extensions try { var consoleService = new ConsoleServiceFromDebuggerServices(DebuggerServices); - _serviceProvider.AddService(consoleService); + var fileLoggingConsoleService = new FileLoggingConsoleService(consoleService); + DiagnosticLoggingService.Instance.SetConsole(consoleService, fileLoggingConsoleService); + _serviceProvider.AddService(fileLoggingConsoleService); + _serviceProvider.AddService(fileLoggingConsoleService); _contextService = new ContextServiceFromDebuggerServices(this, DebuggerServices); _serviceProvider.AddService(_contextService); diff --git a/src/SOS/SOS.UnitTests/SOSRunner.cs b/src/SOS/SOS.UnitTests/SOSRunner.cs index 616733bfa..92e59229d 100644 --- a/src/SOS/SOS.UnitTests/SOSRunner.cs +++ b/src/SOS/SOS.UnitTests/SOSRunner.cs @@ -1412,6 +1412,9 @@ public class SOSRunner : IDisposable vars.Add("%DUMP_NAME%", dumpFileName); } vars.Add("%DEBUG_ROOT%", debuggeeConfig.BinaryDirPath); + vars.Add("%TEST_NAME%", information.TestName); + vars.Add("%LOG_PATH%", information.TestConfiguration.LogDirPath); + vars.Add("%LOG_SUFFIX%", information.TestConfiguration.LogSuffix); vars.Add("%SOS_PATH%", information.TestConfiguration.SOSPath()); vars.Add("%DESKTOP_RUNTIME_PATH%", information.TestConfiguration.DesktopRuntimePath()); diff --git a/src/SOS/SOS.UnitTests/Scripts/OtherCommands.script b/src/SOS/SOS.UnitTests/Scripts/OtherCommands.script index 32fb62b9e..f90ca8f5b 100644 --- a/src/SOS/SOS.UnitTests/Scripts/OtherCommands.script +++ b/src/SOS/SOS.UnitTests/Scripts/OtherCommands.script @@ -56,9 +56,6 @@ ENDIF:LIVE CONTINUE -EXTCOMMAND:clrmodules -VERIFY:\s*.* - EXTCOMMAND:modules -v VERIFY:\s*\s+.* @@ -129,6 +126,12 @@ VERIFY:\s+MT\s+Count\s+TotalSize\s+Class Name\s+ VERIFY:(\s*\s+\s+\s+.*)? VERIFY:\s*Total\s+\s+objects\s+ +EXTCOMMAND:logopen %LOG_PATH%/%TEST_NAME%.%LOG_SUFFIX%.consolelog +EXTCOMMAND:logging %LOG_PATH%/%TEST_NAME%.%LOG_SUFFIX%.diaglog + +EXTCOMMAND:clrmodules -v +VERIFY:\s*.* + SOSCOMMAND:SyncBlk # On Linux/MacOS we sometimes get "Error requesting SyncBlk data" error from the DAC. IFDEF:WINDOWS @@ -143,3 +146,6 @@ SOSCOMMAND:GCHandles SOSCOMMAND:DumpGCData SOSCOMMAND:DumpRuntimeTypes + +EXTCOMMAND:logclose +EXTCOMMAND:logging --disable diff --git a/src/Tools/dotnet-dump/Analyzer.cs b/src/Tools/dotnet-dump/Analyzer.cs index 069e3af3b..6071b5ac2 100644 --- a/src/Tools/dotnet-dump/Analyzer.cs +++ b/src/Tools/dotnet-dump/Analyzer.cs @@ -23,7 +23,8 @@ namespace Microsoft.Diagnostics.Tools.Dump public class Analyzer : IHost { private readonly ServiceProvider _serviceProvider; - private readonly ConsoleService _consoleProvider; + private readonly ConsoleService _consoleService; + private readonly FileLoggingConsoleService _fileLoggingConsoleService; private readonly CommandService _commandService; private readonly SymbolService _symbolService; private readonly ContextService _contextService; @@ -32,23 +33,26 @@ namespace Microsoft.Diagnostics.Tools.Dump public Analyzer() { - LoggingCommand.Initialize(); + DiagnosticLoggingService.Initialize(); _serviceProvider = new ServiceProvider(); - _consoleProvider = new ConsoleService(); + _consoleService = new ConsoleService(); + _fileLoggingConsoleService = new FileLoggingConsoleService(_consoleService); _commandService = new CommandService(); _symbolService = new SymbolService(this); _contextService = new ContextService(this); + DiagnosticLoggingService.Instance.SetConsole(_fileLoggingConsoleService, _fileLoggingConsoleService); _serviceProvider.AddService(this); - _serviceProvider.AddService(_consoleProvider); + _serviceProvider.AddService(_fileLoggingConsoleService); + _serviceProvider.AddService(_fileLoggingConsoleService); + _serviceProvider.AddService(DiagnosticLoggingService.Instance); _serviceProvider.AddService(_commandService); _serviceProvider.AddService(_symbolService); _serviceProvider.AddService(_contextService); _serviceProvider.AddServiceFactory(() => SOSLibrary.Create(this)); - _contextService.ServiceProvider.AddServiceFactory(() => - { + _contextService.ServiceProvider.AddServiceFactory(() => { ClrRuntime clrRuntime = _contextService.Services.GetService(); return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null; }); @@ -57,13 +61,13 @@ namespace Microsoft.Diagnostics.Tools.Dump _commandService.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly }); _commandService.AddCommands(new Assembly[] { typeof(SOSHost).Assembly }); _commandService.AddCommands(typeof(HelpCommand), (services) => new HelpCommand(_commandService, services)); - _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleProvider.Stop)); + _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleService.Stop)); _commandService.AddCommands(typeof(SOSCommand), (services) => new SOSCommand(_commandService, services)); } public Task Analyze(FileInfo dump_path, string[] command) { - _consoleProvider.WriteLine($"Loading core dump: {dump_path} ..."); + _fileLoggingConsoleService.WriteLine($"Loading core dump: {dump_path} ..."); // Attempt to load the persisted command history string dotnetHome = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".dotnet"); @@ -71,7 +75,7 @@ namespace Microsoft.Diagnostics.Tools.Dump try { string[] history = File.ReadAllLines(historyFileName); - _consoleProvider.AddCommandHistory(history); + _consoleService.AddCommandHistory(history); } catch (Exception ex) when (ex is IOException || @@ -109,20 +113,21 @@ namespace Microsoft.Diagnostics.Tools.Dump foreach (string cmd in command) { _commandService.Execute(cmd, _contextService.Services); - if (_consoleProvider.Shutdown) + if (_consoleService.Shutdown) { break; } } } - if (!_consoleProvider.Shutdown && (!Console.IsOutputRedirected || Console.IsInputRedirected)) + if (!_consoleService.Shutdown && (!Console.IsOutputRedirected || Console.IsInputRedirected)) { // Start interactive command line processing - _consoleProvider.WriteLine("Ready to process analysis commands. Type 'help' to list available commands or 'help [command]' to get detailed help on a command."); - _consoleProvider.WriteLine("Type 'quit' or 'exit' to exit the session."); + _fileLoggingConsoleService.WriteLine("Ready to process analysis commands. Type 'help' to list available commands or 'help [command]' to get detailed help on a command."); + _fileLoggingConsoleService.WriteLine("Type 'quit' or 'exit' to exit the session."); - _consoleProvider.Start((string commandLine, CancellationToken cancellation) => + _consoleService.Start((string prompt, string commandLine, CancellationToken cancellation) => { + _fileLoggingConsoleService.WriteLine("{0}{1}", prompt, commandLine); _commandService.Execute(commandLine, _contextService.Services); }); } @@ -137,7 +142,7 @@ namespace Microsoft.Diagnostics.Tools.Dump ex is InvalidOperationException || ex is NotSupportedException) { - _consoleProvider.WriteLine(OutputType.Error, $"{ex.Message}"); + _fileLoggingConsoleService.WriteError($"{ex.Message}"); return Task.FromResult(1); } finally @@ -149,7 +154,7 @@ namespace Microsoft.Diagnostics.Tools.Dump // Persist the current command history try { - File.WriteAllLines(historyFileName, _consoleProvider.GetCommandHistory()); + File.WriteAllLines(historyFileName, _consoleService.GetCommandHistory()); } catch (Exception ex) when (ex is IOException || @@ -224,12 +229,12 @@ namespace Microsoft.Diagnostics.Tools.Dump } catch (Exception ex) when (ex is IOException || ex is ArgumentException || ex is BadImageFormatException || ex is System.Security.SecurityException) { - _consoleProvider.WriteLineError($"Extension load {extensionPath} FAILED {ex.Message}"); + _fileLoggingConsoleService.WriteLineError($"Extension load {extensionPath} FAILED {ex.Message}"); } if (assembly is not null) { _commandService.AddCommands(assembly); - _consoleProvider.WriteLine($"Extension loaded {extensionPath}"); + _fileLoggingConsoleService.WriteLine($"Extension loaded {extensionPath}"); } } } -- 2.34.1