InformationMessage.cs
using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; using System.Text.RegularExpressions; namespace Information { public class InformationMessage { /// <summary> /// Keep track of when the current invocation started /// </summary> public static DateTimeOffset StartTime { get; set; } /// <summary> /// Track the console display width /// </summary> public static int ExceptionWidth { get; set; } /// <summary> /// The template for formatting the display string /// </summary> public static string InfoTemplate { get; set; } static InformationMessage() { StartTime = DateTimeOffset.MinValue; ExceptionWidth = 120; InfoTemplate = "{ClockTime}{Indent}{Message} <{Command}> {ScriptName}:{LineNumber}"; } /// <summary> /// A prefix to use when converting to string /// </summary> public string PSComputerName { get; set; } /// <summary> /// If set, shows the exception stack /// </summary> public bool ShowException { get; set; } /// <summary> /// A prefix to use when converting to string /// </summary> public string Prefix { get; set; } // The Time is here so we can use it in the InfoTemplate /// <summary> /// The full date and time when this message was generated /// </summary> private DateTimeOffset _generatedDateTime; public DateTimeOffset GeneratedDateTime { get { return _generatedDateTime; } set { _generatedDateTime = value; if (DateTimeOffset.MinValue == StartTime) { StartTime = _generatedDateTime; } ElapsedTime = _generatedDateTime - StartTime; } } /// <summary> /// The difference between the time this was generated and the start time /// </summary> public TimeSpan ElapsedTime { get; set; } /// <summary> /// The Time portion of TimeGenerated /// </summary> public TimeSpan ClockTime { get { return GeneratedDateTime.TimeOfDay; } } // The mandatory constructor parameters /// <summary> /// The original message object passed to Write-Info /// </summary> public PSObject MessageData { get; set; } /// <summary> /// The (script) callstack at the point of creation /// </summary> private Array _callstack; public Array CallStack { get { return _callstack; } set { _callstack = value; if ((_callstack is string[])) { var stack = new List<string>(); foreach (string frames in _callstack) { stack.AddRange(frames.Split(new char[] { '\r', '\n' }, options: StringSplitOptions.RemoveEmptyEntries)); } _callstack = stack.ToArray(); } var frame = _callstack.GetValue(0) as CallStackFrame; if (null != frame) { FunctionName = frame.FunctionName.Trim('<','>'); ScriptPath = frame.ScriptName; LineNumber = frame.ScriptLineNumber; if (null == frame.InvocationInfo) { Command = FunctionName; } else { var commandInfo = frame.InvocationInfo.MyCommand; if (null == commandInfo) { Command = frame.InvocationInfo.InvocationName; } else if (!string.IsNullOrEmpty(commandInfo.Name)) { Command = commandInfo.Name; } else { Command = FunctionName; } } Location = frame.GetScriptLocation(); } else { Location = _callstack.GetValue(0).ToString(); var position = Location.Split(new[] { "at ", ", ", ": line " }, StringSplitOptions.RemoveEmptyEntries); if (position.Length > 2) { try { LineNumber = int.Parse(position[2]); } catch { LineNumber = 0; } } if(position.Length > 1) { ScriptPath = position[1]; } if (position.Length > 0) { FunctionName = position[0]; } Command = FunctionName; } } } /// <summary> /// The call stack depth (mostly for the purpose of indenting in the <see cref="InfoTemplate"/>) /// </summary> public int CallStackDepth { get { return CallStack.Length; } } // Calculated based on the MessageData /// <summary> /// The display string, based on the InfoTemplate and all the other properties /// </summary> public String Message { get; set; } // Calculated based on CallStack /// <summary> /// The name of the function the Information was written from /// </summary> public string FunctionName { get; private set; } /// <summary> /// The script file path the Information was written from /// </summary> public string ScriptPath { get; private set; } /// <summary> /// The script file name the Information was written from /// </summary> public string ScriptName { get { if (string.IsNullOrEmpty(ScriptPath)) { return "."; } else { return ScriptPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Last(); } } } /// <summary> /// The line of the script file the Information was written from /// </summary> public int LineNumber { get; private set; } /// <summary> /// The line position the information was written from /// </summary> public string Location { get; private set; } /// <summary> /// The command the information was written from /// </summary> public string Command { get; private set; } /// <summary> /// The single constructor, so that messageData and callStack must be passed /// </summary> /// <param name="messageData"></param> /// <param name="callStack"></param> /// <param name="prefix"></param> /// <param name="simple"></param> public InformationMessage(PSObject messageData, Array callStack, string prefix = "", bool simple = false) { PSComputerName = Environment.GetEnvironmentVariable("ComputerName"); ShowException = !simple; Prefix = prefix ?? ""; GeneratedDateTime = DateTimeOffset.Now; if (messageData.BaseObject is ActionPreferenceStopException && null != ((ActionPreferenceStopException)messageData.BaseObject).ErrorRecord) { MessageData = new PSObject(((ActionPreferenceStopException)messageData.BaseObject).ErrorRecord); } else { MessageData = messageData; } CallStack = callStack; } public override string ToString() { var msg = new StringBuilder(); if(MessageData.BaseObject is string) { if (!string.IsNullOrEmpty(Prefix)) { msg.Append(Prefix); } var stringMessage = MessageData.BaseObject.ToString().Trim(); msg.Append(stringMessage); if (stringMessage.Contains("\n")) { msg.Append("\n "); } Message = msg.ToString(); return ExpandTemplate(); } if (string.IsNullOrEmpty(Prefix)) { if (MessageData.TypeNames.Any(name => name.Contains("System.Management.Automation.RemotingErrorRecord"))) { msg.Append("REMOTE ERROR: "); } else if (MessageData.TypeNames.Any(name => name.Contains("System.Management.Automation.ErrorRecord"))) { msg.Append("MessageData: "); } else if (MessageData.TypeNames.Any(name => name.Contains("Exception"))) { msg.Append("EXCEPTION: "); } } else { msg.Append(Prefix); } // This has to work with both Exceptions and Deserialized Exceptions if (ShowException) { msg.Append(ExpandException(MessageData)); } else { msg.AppendFormat("[{0}]\n\n", MessageData.TypeNames.First()); string MessageDataString; if (LanguagePrimitives.TryConvertTo<string>(MessageData, out MessageDataString)) { msg.AppendLine(MessageDataString); } else { foreach (var property in MessageData.Properties) { // This list is types which aren't worth displaying without a label if (!(property.Value is Boolean || property.Value is Byte || property.Value is SByte || property.Value is Char || property.Value is Single || property.Value is Int16 || property.Value is UInt16 || property.Value is Int32 || property.Value is UInt32 || property.Value is Int64 || property.Value is UInt64 || property.Value is Double || property.Value is Decimal)) { msg.AppendFormat("{0} ", property.Value); } } } return msg.ToString(); } Message = msg.ToString(); return ExpandTemplate(); } public static string ExpandException(PSObject error) { var msg = new StringBuilder(); if (!error.TypeNames.Any(name => name.Contains("System.Management.Automation.ErrorRecord") || name.Contains("System.Exception"))) { msg.AppendFormat("[{0}]\n\n", error.TypeNames.First()); string errorString; if(LanguagePrimitives.TryConvertTo<string>(error, out errorString)) { msg.AppendLine(errorString); } else { foreach (var property in error.Properties) { // This list is types which aren't worth displaying without a label if (!(property.Value is Boolean || property.Value is Byte || property.Value is SByte || property.Value is Char || property.Value is Single || property.Value is Int16 || property.Value is UInt16 || property.Value is Int32 || property.Value is UInt32 || property.Value is Int64 || property.Value is UInt64 || property.Value is Double || property.Value is Decimal)) { msg.AppendFormat("{0} ", property.Value); } } } return msg.ToString(); } msg.AppendLine(error.Properties.Any(p => p.Name == "Exception") ? new PSObject(error.Properties.First(p => p.Name == "Exception").Value).Properties.First(p => p.Name == "Message").Value.ToString() : error.Properties.First(p => p.Name == "Message").Value.ToString()); msg.AppendLine(); // Render the nested errors directly into the message var width = ExceptionWidth; var level = 1; var left = 0; while (null != error) { PSObject next = null; var stackTrace = new StringBuilder(); msg.AppendFormat("{0}[{1}]\n\n", " ".PadLeft(level * 4), error.TypeNames.First(name => name.Contains("System.Management.Automation.ErrorRecord") || name.Contains("System.Exception"))); // I'm hard-coding skipping this one property because it's name is long and it's pointless left = error.Properties.Max(p => p.Name.Contains("WasThrownFromThrowStatement") ? 0 : p.Name.Length); foreach (var property in error.Properties) { if (string.IsNullOrWhiteSpace("" + property.Value) || property.Name == "WasThrownFromThrowStatement") { continue; } if ((property.Name == "Exception" || property.Name == "InnerException") && property.Value != null) { next = new PSObject( property.Value ); } else if (property.Name.EndsWith("StackTrace")) { // we track the stacktrace separately so we can put it last, because it's multi-line var value = ("" + property.Value).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); stackTrace.AppendLine(" ".PadLeft(level * 4) + (property.Name).PadRight(left) + " : " + value.First()); foreach (var additional in value.Skip(1)) { stackTrace.AppendLine(" ".PadLeft((level * 4) + left + 3 ) + additional.Trim()); } } else { msg.AppendLine(" ".PadLeft(level * 4) + (property.Name).PadRight(left) + " : " + property.Value); } } // Stick a blank line on the end ... after the stackTrace msg.AppendLine(stackTrace.ToString()); error = next; level++; width -= 4; } msg.Append(" ".PadLeft((level * 4))); return msg.ToString(); } private string ExpandTemplate() { var message = InfoTemplate; message = Regex.Replace(message, @"{ClockTime}", ClockTime.ToString(@"hh\:mm\:ss\.ffffff"), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{ClockTime:(.+?)}", m => ClockTime.ToString(m.Groups[1].Value.Replace(":",@"\:").Replace(".", @"\.").Replace("-", @"\-")), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{ElapsedTime}", ElapsedTime.ToString(@"hh\:mm\:ss\.ffffff"), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{ElapsedTime:(.+?)}", m => ElapsedTime.ToString(m.Groups[1].Value.Replace(":", @"\:").Replace(".", @"\.").Replace("-", @"\-")), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{GeneratedDateTime}", GeneratedDateTime.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{GeneratedDateTime:(.+?)}", m => GeneratedDateTime.ToString(m.Groups[1].Value.Replace(":", @"\:").Replace(".", @"\.").Replace("-", @"\-")), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{PSComputerName}", PSComputerName.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{CallStack}", CallStack.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{Command}", Command.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{FunctionName}", FunctionName.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{Indent}", " ".PadLeft(CallStackDepth * 2), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{LineNumber}", LineNumber.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{Location}", Location.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{Message}", Message.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{ScriptName}", ScriptName.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{ScriptPath}", ScriptPath.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"{TimeGenerated}", GeneratedDateTime.ToString(), RegexOptions.IgnoreCase); message = Regex.Replace(message, @"`e", "\u001b", RegexOptions.IgnoreCase); return message; } } } /* hidden [void] init([PSObject]$MessageData, [Array]$CallStack, [string]$Prefix, [bool]$Simple) { Write-Warning "ENTER init Information.InformationMessage($MessageData, $CallStack)" } [string]ToString() { $e = [char]27 # Copy everything into local variables so they work in ExpandString $local:MessageData = $this.MessageData $local:Message = $this.Message $local:CallStack = $this.CallStack $local:TimeGenerated = $this.TimeGenerated $local:ElapsedTime = $this.ElapsedTime $local:Time = $this.Time $local:FunctionName = $this.FunctionName $local:ScriptPath = $this.ScriptPath $local:LineNumber = $this.LineNumber $local:Command = $this.Command $local:Location = $this.Location $local:Arguments = $this.Arguments $local:ScriptName = $this.ScriptName $local:CallStackDepth = $this.CallStackDepth try { return (Get-Variable ExecutionContext -ValueOnly).InvokeCommand.ExpandString( [Information.InformationMessage]::InfoTemplate ) } catch { Write-Warning $_ return "{0} {1} at {2}" -f $this.Time, $this.Message, $this.Location } } } Update-TypeData -TypeName Information.InformationMessage -SerializationMethod 'AllPublicProperties' -SerializationDepth 4 -Force Update-TypeData -TypeName System.Management.Automation.InformationRecord -SerializationMethod 'AllPublicProperties' -SerializationDepth 6 -Force */ |