diff --git a/src/PowerShellEditorServices.Host/Program.cs b/src/PowerShellEditorServices.Host/Program.cs
index d6ac8f619..da123dbec 100644
--- a/src/PowerShellEditorServices.Host/Program.cs
+++ b/src/PowerShellEditorServices.Host/Program.cs
@@ -4,6 +4,7 @@
//
using Microsoft.PowerShell.EditorServices.Transport.Stdio;
+using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Diagnostics;
using System.Linq;
@@ -38,11 +39,30 @@ static void Main(string[] args)
}
}
#endif
+ // Catch unhandled exceptions for logging purposes
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+
+ // Initialize the logger
+ // TODO: Set the level based on command line parameter
+ Logger.Initialize(minimumLogLevel: LogLevel.Verbose);
+ Logger.Write(LogLevel.Normal, "PowerShell Editor Services Host started!");
// TODO: Select host, console host, and transport based on command line arguments
IHost host = new StdioHost();
host.Start();
}
+
+ static void CurrentDomain_UnhandledException(
+ object sender,
+ UnhandledExceptionEventArgs e)
+ {
+ // Log the exception
+ Logger.Write(
+ LogLevel.Error,
+ string.Format(
+ "FATAL UNHANDLED EXCEPTION:\r\n\r\n{0}",
+ e.ExceptionObject.ToString()));
+ }
}
}
diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs
index 6cdc3a616..b1e142130 100644
--- a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs
+++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageParser.cs
@@ -45,6 +45,13 @@ public MessageBase ParseMessage(string messageJson)
// Parse the JSON string to a JObject
JObject messageObject = JObject.Parse(messageJson);
+ // Log the message
+ Logger.Write(
+ LogLevel.Verbose,
+ string.Format(
+ "PARSE MESSAGE:\r\n\r\n{0}",
+ messageObject.ToString(Formatting.Indented)));
+
// Get the message type and name from the JSON object
if (!this.TryGetMessageTypeAndName(
messageObject,
diff --git a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs
index 443ec77f4..4bafaf5f1 100644
--- a/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs
+++ b/src/PowerShellEditorServices.Transport.Stdio/Message/MessageWriter.cs
@@ -5,6 +5,7 @@
using Microsoft.PowerShell.EditorServices.Utility;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using System.IO;
using System.Text;
@@ -18,6 +19,10 @@ public class MessageWriter
private bool includeContentLength;
private MessageTypeResolver messageTypeResolver;
+ private JsonSerializer loggingSerializer =
+ JsonSerializer.Create(
+ Constants.JsonSerializerSettings);
+
#endregion
#region Constructors
@@ -57,8 +62,18 @@ public void WriteMessage(MessageBase messageToWrite)
// Insert the message's type name before serializing
messageToWrite.PayloadType = messageTypeName;
+ // Log the JSON representation of the message
+ Logger.Write(
+ LogLevel.Verbose,
+ string.Format(
+ "WRITE MESSAGE:\r\n\r\n{0}",
+ JsonConvert.SerializeObject(
+ messageToWrite,
+ Formatting.Indented,
+ Constants.JsonSerializerSettings)));
+
// Serialize the message
- string serializedMessage =
+ string serializedMessage =
JsonConvert.SerializeObject(
messageToWrite,
Constants.JsonSerializerSettings);
diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj
index cce64db17..91d056b18 100644
--- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj
+++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj
@@ -90,6 +90,7 @@
+
diff --git a/src/PowerShellEditorServices/Utility/Logger.cs b/src/PowerShellEditorServices/Utility/Logger.cs
new file mode 100644
index 000000000..0d8e89909
--- /dev/null
+++ b/src/PowerShellEditorServices/Utility/Logger.cs
@@ -0,0 +1,180 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Microsoft.PowerShell.EditorServices.Utility
+{
+ ///
+ /// Defines the level indicators for log messages.
+ ///
+ public enum LogLevel
+ {
+ ///
+ /// Indicates a verbose log message.
+ ///
+ Verbose,
+
+ ///
+ /// Indicates a normal, non-verbose log message.
+ ///
+ Normal,
+
+ ///
+ /// Indicates a warning message.
+ ///
+ Warning,
+
+ ///
+ /// Indicates an error message.
+ ///
+ Error
+ }
+
+ ///
+ /// Provides a simple logging interface. May be replaced with a
+ /// more robust solution at a later date.
+ ///
+ public static class Logger
+ {
+ private static LogWriter logWriter;
+
+ ///
+ /// Initializes the Logger for the current session.
+ ///
+ ///
+ /// Optional. Specifies the path at which log messages will be written.
+ ///
+ ///
+ /// Optional. Specifies the minimum log message level to write to the log file.
+ ///
+ public static void Initialize(
+ string logFilePath = "EditorServices.log",
+ LogLevel minimumLogLevel = LogLevel.Normal)
+ {
+ if (logWriter != null)
+ {
+ logWriter.Dispose();
+ }
+
+ // TODO: Parameterize this
+ logWriter =
+ new LogWriter(
+ minimumLogLevel,
+ logFilePath,
+ true);
+ }
+
+ ///
+ /// Closes the Logger.
+ ///
+ public static void Close()
+ {
+ if (logWriter != null)
+ {
+ logWriter.Dispose();
+ }
+ }
+
+ ///
+ /// Writes a message to the log file.
+ ///
+ /// The level at which the message will be written.
+ /// The message text to be written.
+ /// The name of the calling method.
+ /// The source file path where the calling method exists.
+ /// The line number of the calling method.
+ public static void Write(
+ LogLevel logLevel,
+ string logMessage,
+ [CallerMemberName] string callerName = null,
+ [CallerFilePath] string callerSourceFile = null,
+ [CallerLineNumber] int callerLineNumber = 0)
+ {
+ if (logWriter != null)
+ {
+ logWriter.Write(
+ logLevel,
+ logMessage,
+ callerName,
+ callerSourceFile,
+ callerLineNumber);
+ }
+ }
+ }
+
+ internal class LogWriter : IDisposable
+ {
+ private TextWriter textWriter;
+ private LogLevel minimumLogLevel = LogLevel.Verbose;
+
+ public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisting)
+ {
+ this.minimumLogLevel = minimumLogLevel;
+
+ // Ensure that we have a usable log file path
+ if (!Path.IsPathRooted(logFilePath))
+ {
+ logFilePath =
+ Path.Combine(
+ AppDomain.CurrentDomain.BaseDirectory,
+ logFilePath);
+ }
+
+ // Open the log file for writing with UTF8 encoding
+ this.textWriter =
+ new StreamWriter(
+ new FileStream(
+ logFilePath,
+ deleteExisting ?
+ FileMode.Create :
+ FileMode.Append),
+ Encoding.UTF8);
+ }
+
+ public void Write(
+ LogLevel logLevel,
+ string logMessage,
+ string callerName = null,
+ string callerSourceFile = null,
+ int callerLineNumber = 0)
+ {
+ if (logLevel >= this.minimumLogLevel)
+ {
+ // Print the timestamp and log level
+ this.textWriter.WriteLine(
+ "{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n",
+ DateTime.Now,
+ logLevel.ToString().ToUpper(),
+ callerName,
+ callerLineNumber,
+ callerSourceFile);
+
+ // Print out indented message lines
+ foreach (var messageLine in logMessage.Split('\n'))
+ {
+ this.textWriter.WriteLine(" " + messageLine.TrimEnd());
+ }
+
+ // Finish with a newline and flush the writer
+ this.textWriter.WriteLine();
+ this.textWriter.Flush();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.textWriter != null)
+ {
+ this.textWriter.Flush();
+ this.textWriter.Dispose();
+ this.textWriter = null;
+ }
+ }
+ }
+}
diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj
index bc4a81fc8..787df9c8f 100644
--- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj
+++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj
@@ -64,6 +64,7 @@
+
diff --git a/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs
new file mode 100644
index 000000000..ec2bdba9c
--- /dev/null
+++ b/test/PowerShellEditorServices.Test/Utility/LoggerTests.cs
@@ -0,0 +1,123 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.PowerShell.EditorServices.Utility;
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace Microsoft.PowerShell.EditorServices.Test.Utility
+{
+ public class LoggerTests
+ {
+ private const string testMessage = "This is a test log message.";
+ private readonly string logFilePath =
+ Path.Combine(
+ AppDomain.CurrentDomain.BaseDirectory,
+ "Test.log");
+
+ [Fact]
+ public void WritesNormalLogMessage()
+ {
+ this.AssertWritesMessageAtLevel(LogLevel.Normal);
+ }
+
+ [Fact]
+ public void WritesVerboseLogMessage()
+ {
+ this.AssertWritesMessageAtLevel(LogLevel.Verbose);
+ }
+
+ [Fact]
+ public void WritesWarningLogMessage()
+ {
+ this.AssertWritesMessageAtLevel(LogLevel.Warning);
+ }
+
+ [Fact]
+ public void WritesErrorLogMessage()
+ {
+ this.AssertWritesMessageAtLevel(LogLevel.Error);
+ }
+
+ [Fact]
+ public void CanExcludeMessagesBelowNormalLevel()
+ {
+ this.AssertExcludesMessageBelowLevel(LogLevel.Normal);
+ }
+
+ [Fact]
+ public void CanExcludeMessagesBelowWarningLevel()
+ {
+ this.AssertExcludesMessageBelowLevel(LogLevel.Warning);
+ }
+
+ [Fact]
+ public void CanExcludeMessagesBelowErrorLevel()
+ {
+ this.AssertExcludesMessageBelowLevel(LogLevel.Error);
+ }
+
+ #region Helper Methods
+
+ private void AssertWritesMessageAtLevel(LogLevel logLevel)
+ {
+ // Write a message at the desired level
+ Logger.Initialize(logFilePath, LogLevel.Verbose);
+ Logger.Write(logLevel, testMessage);
+
+ // Read the contents and verify that it's there
+ string logContents = this.ReadLogContents();
+ Assert.Contains(this.GetLogLevelName(logLevel), logContents);
+ Assert.Contains(testMessage, logContents);
+ }
+
+ private void AssertExcludesMessageBelowLevel(LogLevel minimumLogLevel)
+ {
+ Logger.Initialize(logFilePath, minimumLogLevel);
+
+ // Get all possible log levels
+ LogLevel[] allLogLevels =
+ Enum.GetValues(typeof(LogLevel))
+ .Cast()
+ .ToArray();
+
+ // Write a message at each log level
+ foreach (var logLevel in allLogLevels)
+ {
+ Logger.Write((LogLevel)logLevel, testMessage);
+ }
+
+ // Make sure all excluded log levels aren't in the contents
+ string logContents = this.ReadLogContents();
+ for (int i = 0; i < (int)minimumLogLevel; i++)
+ {
+ LogLevel logLevel = allLogLevels[i];
+ Assert.DoesNotContain(this.GetLogLevelName(logLevel), logContents);
+ }
+ }
+
+ private string GetLogLevelName(LogLevel logLevel)
+ {
+ return logLevel.ToString().ToUpper();
+ }
+
+ private string ReadLogContents()
+ {
+ Logger.Close();
+
+ return
+ string.Join(
+ "\r\n",
+ File.ReadAllLines(
+ logFilePath,
+ Encoding.UTF8));
+ }
+
+ #endregion
+ }
+}