//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace BansheeEngine
{
/** @addtogroup Utility
* @{
*/
///
/// Possible types of debug messages.
///
public enum DebugMessageType // Note: Must match C++ enum DebugChannel
{
Info, Warning, Error, CompilerWarning, CompilerError
}
///
/// Contains data for a single entry in a call stack associated with a log entry.
///
public class CallStackEntry
{
public string method;
public string file;
public int line;
}
///
/// Contains data for a single log entry. Contained data was parsed from the message string
/// to better organize the provided information.
///
public class ParsedLogEntry
{
public string message;
public CallStackEntry[] callstack;
}
///
/// Contains data for a single log entry.
///
[StructLayout(LayoutKind.Sequential)]
public struct LogEntry // Note: Must match C++ struct ScriptLogEntryData
{
public DebugMessageType type;
public string message;
}
///
/// Utility class providing various debug functionality.
///
public sealed class Debug
{
///
/// Triggered when a new message is added to the debug log.
///
public static Action OnAdded;
///
/// Returns a list of all messages in the debug log.
///
public static LogEntry[] Messages
{
get { return Internal_GetMessages(); }
}
///
/// Logs a new informative message to the global debug log.
///
/// Message to log.
public static void Log(object message)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(message.ToString());
sb.Append(GetStackTrace(1));
Internal_Log(sb.ToString());
}
///
/// Logs a new warning message to the global debug log.
///
/// Message to log.
public static void LogWarning(object message)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(message.ToString());
sb.Append(GetStackTrace(1));
Internal_LogWarning(sb.ToString());
}
///
/// Logs a new error message to the global debug log.
///
/// Message to log.
public static void LogError(object message)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(message.ToString());
sb.Append(GetStackTrace(1));
Internal_LogError(sb.ToString());
}
///
/// Logs a new message to the global debug log using the provided type.
///
/// Message to log.
/// Type of the message to log.
internal static void LogMessage(object message, DebugMessageType type)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(message.ToString());
Internal_LogMessage(sb.ToString(), type);
}
///
/// Clears all messages from the debug log.
///
internal static void Clear()
{
Internal_Clear();
}
///
/// Clears all messages of the specified type from the debug log.
///
internal static void Clear(DebugMessageType type)
{
Internal_ClearType(type);
}
///
/// Returns the stack trace of the current point in code.
///
/// Determines how many of the top-level entries should be ignored.
/// String containing the stack trace.
public static string GetStackTrace(int ignoreFirstN = 0)
{
StackTrace stackTrace = new StackTrace(1, true);
StackFrame[] frames = stackTrace.GetFrames();
if (frames == null)
return "";
StringBuilder sb = new StringBuilder();
for(int i = 0; i < frames.Length; i++)
{
if (i < ignoreFirstN)
continue;
StackFrame frame = frames[i];
MethodBase method = frame.GetMethod();
if (method == null)
continue;
Type parentType = method.DeclaringType;
if (parentType == null)
continue;
sb.Append("\tat " + parentType.Name + "." + method.Name + "(");
ParameterInfo[] methodParams = method.GetParameters();
for(int j = 0; j < methodParams.Length; j++)
{
if (j > 0)
sb.Append(", ");
sb.Append(methodParams[j].ParameterType.Name);
}
sb.Append(")");
string ns = parentType.Namespace;
string fileName = frame.GetFileName();
if (!string.IsNullOrEmpty(fileName))
{
int line = frame.GetFileLineNumber();
int column = frame.GetFileColumnNumber();
sb.Append(" in " + fileName + ", line " + line + ", column " + column + ", namespace " + ns);
}
else
{
if (!string.IsNullOrEmpty(ns))
sb.Append(" in namespace " + ns);
}
sb.AppendLine();
}
return sb.ToString();
}
///
/// Parses a log message and outputs a data object with a separate message and callstack entries.
///
/// Message to parse.
/// Parsed log message.
public static ParsedLogEntry ParseLogMessage(string message)
{
// Note: If you are modifying GetStackTrace method make sure to also update this one to match the formattting
int firstMatchIdx = -1;
Regex regex = new Regex(@"\tat (.*) in (.*), line (\d*), column .*, namespace .*");
var matches = regex.Matches(message);
ParsedLogEntry newEntry = new ParsedLogEntry();
newEntry.callstack = new CallStackEntry[matches.Count];
for (int i = 0; i < matches.Count; i++)
{
CallStackEntry callstackEntry = new CallStackEntry();
callstackEntry.method = matches[i].Groups[1].Value;
callstackEntry.file = matches[i].Groups[2].Value;
int.TryParse(matches[i].Groups[3].Value, out callstackEntry.line);
newEntry.callstack[i] = callstackEntry;
if (firstMatchIdx == -1)
firstMatchIdx = matches[i].Index;
}
if (firstMatchIdx != -1)
newEntry.message = message.Substring(0, firstMatchIdx);
else
newEntry.message = message;
return newEntry;
}
///
/// Parses a managed exception message and outputs a data object with a separate message and callstack entries.
///
/// Message to parse.
/// Parsed log message.
public static ParsedLogEntry ParseExceptionMessage(string message)
{
Regex headerRegex = new Regex(@"Managed exception: (.*)\n");
var headerMatch = headerRegex.Match(message);
if (!headerMatch.Success)
return null;
Regex regex = new Regex(@" at (.*) \[.*\] in (.*):(\d*)");
var matches = regex.Matches(message);
ParsedLogEntry newEntry = new ParsedLogEntry();
newEntry.callstack = new CallStackEntry[matches.Count];
for (int i = 0; i < matches.Count; i++)
{
CallStackEntry callstackEntry = new CallStackEntry();
callstackEntry.method = matches[i].Groups[1].Value;
callstackEntry.file = matches[i].Groups[2].Value;
int.TryParse(matches[i].Groups[3].Value, out callstackEntry.line);
newEntry.callstack[i] = callstackEntry;
}
newEntry.message = headerMatch.Groups[1].Value;
return newEntry;
}
///
/// Triggered by the runtime when a new message is added to the debug log.
///
/// Type of the message that was added.
/// Text of the message.
private static void Internal_OnAdded(DebugMessageType type, string message)
{
if (OnAdded != null)
OnAdded(type, message);
}
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_Log(string message);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_LogWarning(string message);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_LogError(string message);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_LogMessage(string message, DebugMessageType type);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_Clear();
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Internal_ClearType(DebugMessageType type);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern LogEntry[] Internal_GetMessages();
}
/** @} */
}