Add console file logging (#3358)
authorMike McLaughlin <mikem@microsoft.com>
Fri, 9 Sep 2022 22:25:25 +0000 (15:25 -0700)
committerGitHub <noreply@github.com>
Fri, 9 Sep 2022 22:25:25 +0000 (15:25 -0700)
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

16 files changed:
src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/Tracer.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.DebugServices/Tracer.cs [deleted file]
src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs
src/Microsoft.Diagnostics.Repl/ConsoleService.cs
src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs
src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs
src/SOS/SOS.Extensions/HostServices.cs
src/SOS/SOS.UnitTests/SOSRunner.cs
src/SOS/SOS.UnitTests/Scripts/OtherCommands.script
src/Tools/dotnet-dump/Analyzer.cs

diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/DiagnosticLoggingService.cs
new file mode 100644 (file)
index 0000000..94ee4a3
--- /dev/null
@@ -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);
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs
new file mode 100644 (file)
index 0000000..ea041ed
--- /dev/null
@@ -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
+    }
+}
index 3f62861b23dee1d9e12f4de1bdf2393117a95429..58b453f1bd34a47d12ae84863b4d471b73da6a91 100644 (file)
@@ -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.Implementation/Tracer.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Tracer.cs
new file mode 100644 (file)
index 0000000..6a7d593
--- /dev/null
@@ -0,0 +1,127 @@
+// 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.SymbolStore;
+using System.Diagnostics;
+
+namespace Microsoft.Diagnostics.DebugServices.Implementation
+{
+    /// <summary>
+    /// Simple trace/logging support.
+    /// </summary>
+    public sealed class Tracer : ITracer
+    {
+        public static bool Enable { get; set; }
+
+        public static ITracer Instance { get; } = Enable ? new Tracer() : new NullTracer();
+
+        private Tracer()
+        {
+        }
+
+        public void WriteLine(string message)
+        {
+            Trace.WriteLine(message);
+            Trace.Flush();
+        }
+
+        public void WriteLine(string format, params object[] arguments)
+        {
+            WriteLine(string.Format(format, arguments));
+        }
+
+        public void Information(string message)
+        {
+            Trace.TraceInformation(message);
+            Trace.Flush();
+        }
+
+        public void Information(string format, params object[] arguments)
+        {
+            Trace.TraceInformation(format, arguments);
+            Trace.Flush();
+        }
+
+        public void Warning(string message)
+        {
+            Trace.TraceWarning(message);
+            Trace.Flush();
+        }
+
+        public void Warning(string format, params object[] arguments)
+        {
+            Trace.TraceWarning(format, arguments);
+            Trace.Flush();
+        }
+
+        public void Error(string message)
+        {
+            Trace.TraceError(message);
+            Trace.Flush();
+        }
+
+        public void Error(string format, params object[] arguments)
+        {
+            Trace.TraceError(format, arguments);
+            Trace.Flush();
+        }
+
+        public void Verbose(string message)
+        {
+            Information(message);
+        }
+
+        public void Verbose(string format, params object[] arguments)
+        {
+            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 (file)
index 0000000..5859286
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Console file logging control service
+    /// </summary>
+    public interface IConsoleFileLoggingService
+    {
+        /// <summary>
+        /// The log file path if enabled, otherwise null.
+        /// </summary>
+        string FilePath { get; }
+
+        /// <summary>
+        /// Enable console file logging.
+        /// </summary>
+        /// <param name="filePath">log file path</param>
+        /// <remarks>see File.Open for possible exceptions thrown</remarks>
+        void Enable(string filePath);
+
+        /// <summary>
+        /// Disable/close console file logging.
+        /// </summary>
+        void Disable();
+
+        /// <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>
+        void AddStream(Stream stream);
+
+        /// <summary>
+        /// Remove the specified file stream from the writers.
+        /// </summary>
+        /// <param name="stream">Stream passed to add. Stream not closed or disposed.</param>
+        void RemoveStream(Stream stream);
+    }
+}
diff --git a/src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs b/src/Microsoft.Diagnostics.DebugServices/IDiagnosticLoggingService.cs
new file mode 100644 (file)
index 0000000..de2ed45
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Service to control the internal diagnostic (Trace) logging.
+    /// </summary>
+    public interface IDiagnosticLoggingService
+    {
+        /// <summary>
+        /// Returns true if logging to console or file
+        /// </summary>
+        bool IsEnabled { get; }
+
+        /// <summary>
+        /// The file path if logging to file.
+        /// </summary>
+        string FilePath { get; }
+
+        /// <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>
+        void Enable(string filePath);
+
+        /// <summary>
+        /// Disable diagnostics logging (close if logging to file).
+        /// </summary>
+        void Disable();
+    }
+}
diff --git a/src/Microsoft.Diagnostics.DebugServices/Tracer.cs b/src/Microsoft.Diagnostics.DebugServices/Tracer.cs
deleted file mode 100644 (file)
index 516d495..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-// 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.Diagnostics;
-
-namespace Microsoft.Diagnostics.DebugServices
-{
-    /// <summary>
-    /// Simple trace/logging support.
-    /// </summary>
-    public sealed class Tracer : Microsoft.SymbolStore.ITracer
-    {
-        public static Microsoft.SymbolStore.ITracer Instance { get; } = new Tracer();
-
-        private Tracer()
-        {
-        }
-
-        public void WriteLine(string message)
-        {
-            Trace.WriteLine(message);
-            Trace.Flush();
-        }
-
-        public void WriteLine(string format, params object[] arguments)
-        {
-            WriteLine(string.Format(format, arguments));
-        }
-
-        public void Information(string message)
-        {
-            Trace.TraceInformation(message);
-            Trace.Flush();
-        }
-
-        public void Information(string format, params object[] arguments)
-        {
-            Trace.TraceInformation(format, arguments);
-            Trace.Flush();
-        }
-
-        public void Warning(string message)
-        {
-            Trace.TraceWarning(message);
-            Trace.Flush();
-        }
-            
-        public void Warning(string format, params object[] arguments)
-        {
-            Trace.TraceWarning(format, arguments);
-            Trace.Flush();
-        }
-
-        public void Error(string message)
-        {
-            Trace.TraceError(message);
-            Trace.Flush();
-        }
-
-        public void Error(string format, params object[] arguments)
-        {
-            Trace.TraceError(format, arguments);
-            Trace.Flush();
-        }
-
-        public void Verbose(string message)
-        {
-            Information(message);
-        }
-
-        public void Verbose(string format, params object[] arguments)
-        {
-            Information(format, arguments);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs
new file mode 100644 (file)
index 0000000..18eb2cb
--- /dev/null
@@ -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");
+            }
+        }
+    }
+}
index 974a7968166196690568cc50fc4954f87852aa23..11f969b8d80adb3f15dac52c0984a1bea5c06bca 100644 (file)
@@ -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);
             }
         }
     }
index 27bd1b4223ef0d7e80be578005b6c488842ef3ac..c14fba57b7ced8c2a8cebed9e9d34f5b6b76dfc9 100644 (file)
@@ -73,7 +73,7 @@ namespace Microsoft.Diagnostics.Repl
         /// Start input processing and command dispatching
         /// </summary>
         /// <param name="dispatchCommand">Called to dispatch a command on ENTER</param>
-        public void Start(Action<string, CancellationToken> dispatchCommand)
+        public void Start(Action<string, string, CancellationToken> dispatchCommand)
         {
             m_lastCommandLine = null;
             m_interactiveConsole = !Console.IsInputRedirected;
@@ -159,14 +159,6 @@ namespace Microsoft.Diagnostics.Repl
             RefreshLine();
         }
 
-        /// <summary>
-        /// Writes a message with a new line to console.
-        /// </summary>
-        public void WriteLine(string format, params object[] parameters)
-        {
-            WriteLine(OutputType.Normal, format, parameters);
-        }
-
         /// <summary>
         /// Writes a message with a new line to console.
         /// </summary>
@@ -338,7 +330,7 @@ namespace Microsoft.Diagnostics.Repl
             m_refreshingLine = false;
         }
 
-        private void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action<string, CancellationToken> dispatchCommand)
+        private void ProcessKeyInfo(ConsoleKeyInfo keyInfo, Action<string, string, CancellationToken> dispatchCommand)
         {
             int activeLineLen = m_activeLine.Length;
 
@@ -455,7 +447,7 @@ namespace Microsoft.Diagnostics.Repl
             }
         }
 
-        private bool Dispatch(string newCommand, Action<string, CancellationToken> dispatchCommand)
+        private bool Dispatch(string newCommand, Action<string, string, CancellationToken> 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;
index 5aa681063e1480322862b78bb7f663ff38f565d6..95b41a190a4f4ba20bd0112eee147b522fa92bbd 100644 (file)
@@ -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<string, string> AllSettings
index bc8e2f7d5c883e2d83144c3d96f2f78000c26d43..2176a6654a52377a44ccd2008157e7b133df71e6 100644 (file)
@@ -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);
             }
index f83d94b6d6e19ee419f99e0d64dab87b51a1f387..b44ebebe35c15ceeac23d96238bb6a6f43b2ab2b 100644 (file)
@@ -54,7 +54,7 @@ namespace SOS.Extensions
             if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) {
                 AssemblyResolver.Enable();
             }
-            LoggingCommand.Initialize();
+            DiagnosticLoggingService.Initialize();
         }
 
         /// <summary>
@@ -107,6 +107,7 @@ namespace SOS.Extensions
         private HostServices()
         {
             _serviceProvider = new ServiceProvider();
+            _serviceProvider.AddService<IDiagnosticLoggingService>(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<IConsoleService>(consoleService);
+                var fileLoggingConsoleService = new FileLoggingConsoleService(consoleService);
+                DiagnosticLoggingService.Instance.SetConsole(consoleService, fileLoggingConsoleService);
+                _serviceProvider.AddService<IConsoleService>(fileLoggingConsoleService);
+                _serviceProvider.AddService<IConsoleFileLoggingService>(fileLoggingConsoleService);
 
                 _contextService = new ContextServiceFromDebuggerServices(this, DebuggerServices);
                 _serviceProvider.AddService<IContextService>(_contextService);
index 616733bfae43f723e31b20724c54ad769b093ce0..92e59229d1c83cdbd958c0f2e1cd1e3c080fee01 100644 (file)
@@ -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());
 
index 32fb62b9eb3743bc495a91444783f5b7c344b094..f90ca8f5b00e67d3c653c02056e425152965e8c1 100644 (file)
@@ -56,9 +56,6 @@ ENDIF:LIVE
 
 CONTINUE
 
-EXTCOMMAND:clrmodules
-VERIFY:\s*<HEXVAL>.*
-
 EXTCOMMAND:modules -v
 VERIFY:\s*<HEXVAL>\s+<HEXVAL>.*
 
@@ -129,6 +126,12 @@ VERIFY:\s+MT\s+Count\s+TotalSize\s+Class Name\s+
 VERIFY:(\s*<HEXVAL>\s+<DECVAL>\s+<DECVAL>\s+.*)?
 VERIFY:\s*Total\s+<DECVAL>\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*<HEXVAL>.*
+
 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
index 069e3af3bf5d3e8cd3cac11d4b9b9036f8fcf13f..6071b5ac287f281dbc6b8b94bbfd08de34e73b0d 100644 (file)
@@ -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<IHost>(this);
-            _serviceProvider.AddService<IConsoleService>(_consoleProvider);
+            _serviceProvider.AddService<IConsoleService>(_fileLoggingConsoleService);
+            _serviceProvider.AddService<IConsoleFileLoggingService>(_fileLoggingConsoleService);
+            _serviceProvider.AddService<IDiagnosticLoggingService>(DiagnosticLoggingService.Instance);
             _serviceProvider.AddService<ICommandService>(_commandService);
             _serviceProvider.AddService<ISymbolService>(_symbolService);
             _serviceProvider.AddService<IContextService>(_contextService);
             _serviceProvider.AddServiceFactory<SOSLibrary>(() => SOSLibrary.Create(this));
 
-            _contextService.ServiceProvider.AddServiceFactory<ClrMDHelper>(() =>
-            {
+            _contextService.ServiceProvider.AddServiceFactory<ClrMDHelper>(() => {
                 ClrRuntime clrRuntime = _contextService.Services.GetService<ClrRuntime>();
                 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<int> 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}");
             }
         }
     }