|
using System; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Reflection.Emit; |
|
using System.Runtime.CompilerServices; |
|
using System.Runtime.InteropServices; |
|
using System.Text; |
|
using System.Threading; |
|
using System.Xml.Linq; |
|
|
|
namespace ConsoleApp12 |
|
{ |
|
class Program |
|
{ |
|
static void Main(string[] args) |
|
{ |
|
//#1: Dump Environment Variables |
|
Environment.GetEnvironmentVariables().Dump("Environment Variables"); |
|
|
|
//#2: Dump ValueTuple |
|
("key", "value").Dump("ValueTuple"); |
|
|
|
//#3: Dump AnonymousClass |
|
new { AAA = 1, BBB = 2, CCC = new { DDD = "DDD" } }.Dump("AnonymousClass"); |
|
} |
|
} |
|
} |
|
|
|
namespace System |
|
{ |
|
public static class HtmlDumper |
|
{ |
|
private static long _id = 0; |
|
private static object @lock = new object(); |
|
|
|
public static T Dump<T>(this T @this, |
|
string title = "", |
|
int maxDepth = 5, |
|
[CallerFilePath] string file = "", |
|
[CallerLineNumber] int line = 0, |
|
[CallerMemberName] string member = "" |
|
) |
|
{ |
|
var id = Interlocked.Increment(ref _id); |
|
var dateTime = DateTimeOffset.UtcNow; |
|
|
|
//Html Dump operation need to be execute with sync |
|
var item = new DumpItem |
|
{ |
|
Id = id, |
|
Title = title, |
|
|
|
//HtmlFragment = htmlFragment, |
|
//RenderingTime = sw.ElapsedMilliseconds, |
|
CallerContext = new CallerContext |
|
{ |
|
AppDomainName = AppDomain.CurrentDomain.FriendlyName, |
|
ThreadId = Thread.CurrentThread.ManagedThreadId, |
|
#if false |
|
//Require NuGet paclage: System.Diagnostics.DiagnosticSource |
|
Activity = Activity.Current?.OperationName, |
|
CorrelationId = Activity.Current?.RootId, |
|
#endif |
|
SynchronizationContextName = SynchronizationContext.Current?.ToString() ?? "", |
|
MemberName = member, |
|
FilePath = file, |
|
LineNumber = line, |
|
}, |
|
Timestamp = dateTime, |
|
}; |
|
|
|
|
|
|
|
string html = null; |
|
|
|
//Step1: Create Dump HTML by LinqPad |
|
var stopwatch = Stopwatch.StartNew(); |
|
{ |
|
html = LinqPadHelper.DumpObject(@this, maxDepth); |
|
} |
|
item.RenderingTime = stopwatch.ElapsedMilliseconds; |
|
|
|
//Step2: Rewrite HTML |
|
stopwatch.Restart(); |
|
{ |
|
html = HtmlHelper.Rewrite($"#{id}:{title}", html, item); |
|
} |
|
//Debug.WriteLine($"Rewrite Time: {stopwatch.ElapsedMilliseconds}[ms]"); |
|
|
|
var tempFileName = IndexHtmlPath.Replace("Index.html", "Index" + id.ToString("0000") + ".html"); |
|
lock (@lock) |
|
{ |
|
//Step3: Write HTML File to %Temp%\HtmlDumper\IndexNNNN.html |
|
File.WriteAllText(tempFileName, html); |
|
|
|
//Step4: Launch chrome window |
|
ChromeLauncher.LaunchNewWindow(tempFileName, ChromeProfilePath); |
|
} |
|
|
|
return @this; |
|
} |
|
|
|
private static string BasePath |
|
{ |
|
get |
|
{ |
|
var path = Path.Combine(Path.GetTempPath(), "HtmlDumper"); |
|
new DirectoryInfo(path).Create(); //Ensure directory exists |
|
return path; |
|
} |
|
} |
|
|
|
private static string IndexHtmlPath => Path.Combine(BasePath, "Index.html"); |
|
private static string ChromeProfilePath = Path.Combine(BasePath, "ChromeProfile"); |
|
|
|
|
|
[Serializable] |
|
internal class DumpItem |
|
{ |
|
public long Id { get; set; } |
|
//public DumpType Type { get; set; } |
|
public string Title { get; set; } |
|
public string HtmlFragment { get; set; } |
|
public long RenderingTime { get; internal set; } |
|
|
|
//Other caller contex related data |
|
public CallerContext CallerContext { get; set; } |
|
|
|
//TODO: Need to specify JSON Serialization format |
|
public DateTimeOffset Timestamp { get; set; } |
|
} |
|
|
|
[Serializable] |
|
internal class CallerContext |
|
{ |
|
public string AppDomainName { get; set; } |
|
public int ThreadId { get; set; } |
|
|
|
//public string Activity { get; set; } |
|
//public string CorrelationId { get; set; } |
|
|
|
public string SynchronizationContextName { get; set; } |
|
|
|
public string MemberName { get; set; } |
|
public string FilePath { get; set; } |
|
public int LineNumber { get; set; } |
|
|
|
//Helper Properties |
|
public bool HasSynchronizationContext => !String.IsNullOrEmpty(SynchronizationContextName); |
|
} |
|
|
|
private class LinqPadHelper |
|
{ |
|
public static string DumpObject<T>(T source, int depth = 5, bool noheader = false) |
|
{ |
|
//Note: Error occurs when call LinqPad method from .NET Core |
|
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Core")) |
|
throw new NotSupportedException(".NET Core platform is not supported when using LinqPad's HTML Dump method"); |
|
|
|
//TODO: XhtmlWriter share internal state (example: table id, totalTextLengh .etc) |
|
var createXhtmlWriter = lazyCache.Value; |
|
using (var writer = createXhtmlWriter(true, depth, noheader)) |
|
{ |
|
// TODO: XhtmlWriter.Write has internal size limit (totalTextLenth: 10000000 || totalObjects > 110000) |
|
// It may be better using XhtmlWriter.FormatObject instead. |
|
writer.Write(source); |
|
return writer.ToString(); |
|
} |
|
} |
|
|
|
//Delegate for Util.CreateXhtmlWriter |
|
private static Lazy<Func<bool, int, bool, TextWriter>> lazyCache = new Lazy<Func<bool, int, bool, TextWriter>>(() => |
|
{ |
|
var type = LinqPadAssembly.Value.GetType("LINQPad.Util"); |
|
var method = new DynamicMethod( |
|
"CreateXhtmlWriter", |
|
typeof(TextWriter), |
|
new Type[] { typeof(bool), typeof(int), typeof(bool) }, |
|
type, |
|
true |
|
); |
|
|
|
var il = method.GetILGenerator(); |
|
il.Emit(OpCodes.Ldarg_0); |
|
il.Emit(OpCodes.Ldarg_1); |
|
il.Emit(OpCodes.Ldarg_2); |
|
il.Emit(OpCodes.Call, type.GetMethod("CreateXhtmlWriter", new Type[] { typeof(bool), typeof(int), typeof(bool) })); |
|
il.Emit(OpCodes.Ret); |
|
|
|
//Cache Util.CreateHtmlWriter delegate |
|
return (Func<bool, int, bool, TextWriter>)method.CreateDelegate(typeof(Func<bool, int, bool, TextWriter>)); |
|
}); |
|
|
|
/// <summary> |
|
/// Load Linqpad assembly |
|
/// </summary> |
|
private static Lazy<Assembly> LinqPadAssembly = new Lazy<Assembly>(() => |
|
{ |
|
//Helper method |
|
string GetLinqPadExePath(Environment.SpecialFolder folder) => Path.Combine(Environment.GetFolderPath(folder), @"LINQPad5\LINQPad.exe"); |
|
|
|
//1. Search LinqPad5 AnyCPU Build instalation path |
|
if (Environment.Is64BitProcess) |
|
{ |
|
var linqPadExePath = GetLinqPadExePath(Environment.SpecialFolder.ProgramFiles); |
|
if (File.Exists(linqPadExePath)) |
|
return Assembly.Load(File.ReadAllBytes(linqPadExePath)); //Load from bytes to avoid assembly file lock |
|
} |
|
|
|
//2. Search LinqPad5 default instalation path |
|
{ |
|
var linqPadExePath = GetLinqPadExePath(Environment.SpecialFolder.ProgramFilesX86); |
|
if (File.Exists(linqPadExePath)) |
|
return Assembly.Load(File.ReadAllBytes(linqPadExePath)); |
|
} |
|
|
|
//If LinqPad.exe is not found at expected path |
|
throw new FileNotFoundException(String.Join(Environment.NewLine, |
|
new[] { |
|
"LINQPad.exe is not found at following locations", |
|
" " + GetLinqPadExePath(Environment.SpecialFolder.ProgramFiles), |
|
" " + GetLinqPadExePath(Environment.SpecialFolder.ProgramFilesX86) |
|
}.Distinct() //Need distinct when called from 32bit process |
|
)); |
|
}); |
|
} |
|
|
|
private static class HtmlHelper |
|
{ |
|
// Use Base64 favicon. from <https://www.freefavicon.com/freefavicons/software/iconinfo/zoom-fit-best-152-195665.html> |
|
const string base64Icon = @"data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIjkhFGCSTgAAAA8AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUdrcHKY3PvDOX2fwkh8hlAA0XAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOdroEKpHUq1C09f9LsPD/PKDh/SCDwzkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHb7YBKJDUmWfM//9my///XcH//y+U1bwegMENAAAAAAAAAAAAAAAAAAAAAAAAAACzsrIBAAAAAAAAAAAAAAAAIIjLcoDk//+D6P//d9z//zKY2rAWerwIAAAAAAAAAAAAAAAAq62vFbu7vInExMTXx8bG8czKyt7IxsWZyMXENcLY6M+B2v//kvL//zyg4qQNdLgFAAAAAAAAAAAAAAAAr7GzM8XGyOjX19f/0NDQ/8zLy//V1NT/4d/e/9fV1P/i4N7/xdjl2DSX2HoGb7UCAAAAAAAAAAAAAAAAra+wEsvMzuTd3t7/7tO9///zsf///6////mw///duv/j4eH/4d/e/8jGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAL/Bwn3n6On/7dK8///////////////i/////////////9qv//Px8P/U0tGkAAAAAAAAAAAAAAAAAAAAAAAAAADV1tfG7O3u///qq//////////9////w////9r////////wmf/r5+b/5OLh67y6uQIAAAAAAAAAAAAAAAAAAAAA4+Tl3ezt7f///7n////6////4P///9X////M////8///+57/8eff//Du7vq6uLcKAAAAAAAAAAAAAAAAAAAAAOXl5sX///////zG///////////////w////+P////////uo//n39f/w7u7qtbOzAgAAAAAAAAAAAAAAAAAAAADQ0dF7///////t3P/////////////////////////////pwv//////4N/fogAAAAAAAAAAAAAAAAAAAAAAAAAAtre3Efr7++P///////bk////5P///+P////N///wzv//////////9LOztCYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+v78w////5f//////////////////////////////8snJyksAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe4uRTo6eqF////0v///+3////Z8fHylLa3tyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAP/gAAD/wAAA/4AAAPuBAADAAwAAgAcAAAAfAAAAHwAAAA8AAAAPAAAADwAAAB8AAAAfAACAPwAAwH8AAA=="; |
|
|
|
public static string Rewrite(string title, string xhtml, DumpItem item) |
|
{ |
|
var doc = XDocument.Parse(xhtml); |
|
doc.Element("html") |
|
.Element("head") |
|
.Add(new XElement("title", title), |
|
new XElement("link", new XAttribute("href", base64Icon), |
|
new XAttribute("rel", "icon"), |
|
new XAttribute("type", "image/x-icon"))); |
|
|
|
//Insert extra data |
|
doc.Element("html") |
|
.Element("body") |
|
.AddFirst(XElement.Parse(ToTableHtml(new[] { item }))); |
|
|
|
|
|
xhtml = doc.ToString(SaveOptions.DisableFormatting); |
|
|
|
return xhtml; |
|
} |
|
|
|
private static string ToTableHtml(DumpItem[] items) |
|
{ |
|
var sb = new StringBuilder(4096); |
|
sb.AppendLine("<table class='table'>") |
|
.AppendLine("<thead>") |
|
.AppendLine(" <th>Id</th>") |
|
.AppendLine(" <th>Title</th>") |
|
.AppendLine(" <th>ThreadId</th>") |
|
.AppendLine(" <th>SynchronizationContext</th>") |
|
.AppendLine(" <th>MemberName</th>") |
|
.AppendLine(" <th>FilePath</th>") |
|
.AppendLine(" <th>LineNumber</th>") |
|
.AppendLine(" <th>RenderTime</th>") |
|
.AppendLine("</thead>") |
|
.AppendLine("<tbody>"); |
|
foreach (var item in items) |
|
{ |
|
sb.AppendLine($"<tr>") |
|
.AppendLine($" <th>#{item.Id}</th>") |
|
.AppendLine($" <td>{item.Title}</td>") |
|
.AppendLine($" <td>{item.CallerContext.ThreadId}</td>") |
|
.AppendLine($" <td>{item.CallerContext.SynchronizationContextName}</td>") |
|
.AppendLine($" <td>{item.CallerContext.MemberName}</td>") |
|
.AppendLine($" <td title='{item.CallerContext.FilePath}'>{Path.GetFileName(item.CallerContext.FilePath)}</td>") |
|
.AppendLine($" <td>{item.CallerContext.LineNumber}</td>") |
|
.AppendLine($" <td>{item.RenderingTime} [ms]</td>") |
|
.AppendLine($"</tr>"); |
|
} |
|
sb.AppendLine("</tbody>") |
|
.AppendLine("</table>"); |
|
|
|
return sb.ToString(); |
|
} |
|
} |
|
|
|
private static class ChromeLauncher |
|
{ |
|
|
|
public static void LaunchNewWindow(string url, string chormeProfilePath) |
|
{ |
|
var chromeExePath = ChromeExePath.Value; |
|
|
|
if (chromeExePath == null) |
|
throw new FileNotFoundException("Couldn't find Chrome installation!"); |
|
|
|
string args = String.Join(" " |
|
, url |
|
//, "--app=" + url |
|
, "--user-data-dir=" + chormeProfilePath |
|
//, "--remote-debugging-port=9222" |
|
//, "--allow-file-access-from-files" |
|
); |
|
var psi = new ProcessStartInfo(chromeExePath, args); |
|
var process = Process.Start(psi); |
|
process.WaitForInputIdle(); |
|
} |
|
|
|
private static Lazy<string> ChromeExePath = new Lazy<string>(() => |
|
{ |
|
//TODO: Support non Windows environment (after removing LinqPad.exe dependency) |
|
//TODO: Search exe path from following registry. but it require NuGet package "Microsoft.Win32.Registry" |
|
//@"\Software\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"; |
|
|
|
//Helper method |
|
string GetChromeExePath(Environment.SpecialFolder folder) => Path.Combine(Environment.GetFolderPath(folder), @"Google\Chrome\Application\chrome.exe"); |
|
|
|
//1. Search "ProgramFiles (x86)" default instalation path (It seems 64 bit chrome use same path) |
|
{ |
|
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.ProgramFilesX86); |
|
if (File.Exists(chromeExePath)) |
|
return chromeExePath; |
|
} |
|
|
|
//2. Search LocalAppData installation path |
|
{ |
|
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.LocalApplicationData); |
|
if (File.Exists(chromeExePath)) |
|
return chromeExePath; |
|
} |
|
|
|
//3. Search "ProgramFiles" instalation path (Should not be used) |
|
if (Environment.Is64BitProcess) |
|
{ |
|
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.ProgramFiles); |
|
if (File.Exists(chromeExePath)) |
|
return chromeExePath; |
|
} |
|
|
|
//If chrome.exe is not found at expected paths |
|
throw new FileNotFoundException(String.Join(Environment.NewLine, |
|
new[] { |
|
"chrome.exe is not found at following expected locations", |
|
" " + GetChromeExePath(Environment.SpecialFolder.ProgramFilesX86), |
|
" " + GetChromeExePath(Environment.SpecialFolder.LocalApplicationData), |
|
" " + GetChromeExePath(Environment.SpecialFolder.CommonProgramFiles) |
|
}.Distinct() //Need distinct called by 32bit process |
|
)); |
|
|
|
|
|
}); |
|
} |
|
} |
|
} |