Tool to get snapshot of managed callstacks

I wrote a simple tool to take a snapshot of a running managed process and dump the output as an XML file. I'll post the full source as a sample on MSDN.
[Update 6/26/06] After great delay, source posted here. Also, check out Managed Stack Explorer, which is a more polished tool that has similar snap-shot gathering behavior.

The usage is pretty simple. To take a snapshot of the running process "hello.exe", run:
    SnapShot.exe -name:hello.exe

And then it dumps out an XML containing callstacks of all threads, including locals and arguments of each frame (see below). 

Comments on the tool:
The actual tool is trivial to write. It's under 500 lines, and the largest part is adding error checking for the command line options and breaking everything out into little xml tags. Here's a watered down basic version of the tool which just attaches (see here for details on attach) and dumps callstacks via MDbgFrame.ToString() (eg, the equivalent of MDbg's where command), and it's under 70 C# lines (Update: fix an issue with draining attach events, bumps the line count up from 50 to 70):

 

//-----------------------------------------------------------------------------
// Harness to snapshot a process's callstacks
// Built on MDbg, Needs a reference to MdbgCore.dll (ships in CLR 2.0 SDK).
// Author: Mike Stall (https://blogs.msdn.com/jmstall)
//-----------------------------------------------------------------------------

using System;
using Microsoft.Samples.Debugging.MdbgEngine;
using System.Diagnostics;

class Program
{
    // Skip past fake attach events. 
    static void DrainAttach(MDbgEngine debugger, MDbgProcess proc)
    {        
        bool fOldStatus = debugger.Options.StopOnNewThread;
        debugger.Options.StopOnNewThread = false; // skip while waiting for AttachComplete

        proc.Go().WaitOne();
        Debug.Assert(proc.StopReason is AttachCompleteStopReason);

        debugger.Options.StopOnNewThread = true; // needed for attach= true; // needed for attach

        // Drain the rest of the thread create events.
        while (proc.CorProcess.HasQueuedCallbacks(null))
        {
            proc.Go().WaitOne();
            Debug.Assert(proc.StopReason is ThreadCreatedStopReason);
        }

        debugger.Options.StopOnNewThread = fOldStatus;
    }

    // Expects 1 arg, the pid as a decimal string
    static void Main(string[] args)
    {
        int pid = int.Parse(args[0]);
        MDbgEngine debugger = new MDbgEngine();
        
        MDbgProcess proc = null;
        try
        {
            proc = debugger.Attach(pid);
            DrainAttach(debugger, proc);            

            MDbgThreadCollection tc = proc.Threads;
            Console.WriteLine("Attached to pid:{0}", pid);
            foreach (MDbgThread t in tc)
            {
                Console.WriteLine("Callstack for Thread {0}", t.Id.ToString());

                foreach (MDbgFrame f in t.Frames)
                {
                    Console.WriteLine("  " + f);
                }
            }
        }
        finally
        {
            if (proc != null) { proc.Detach().WaitOne(); }
        }

    }
}

Some sample output from that is:

Attached to pid:3784
Callstack for Thread 2384
[Internal Frame, 'M-->U']
System.IO.__ConsoleStream.ReadFileNative (source line information unavailable)
System.IO.__ConsoleStream.Read (source line information unavailable)
System.IO.StreamReader.ReadBuffer (source line information unavailable)
System.IO.StreamReader.ReadLine (source line information unavailable)
System.IO.TextReader.SyncTextReader.ReadLine (source line information unavailable)
System.Console.ReadLine (source line information unavailable)
Foo.Main (wait.cs:15)

Some technical notes:
This is doing an invasive attach, running the callstacks, and then doing a detach.  It is not taking a memory dump. Only 1 managed debugger can attach at a time (see here), and you can't do this if a native debugger is already attached (see here). The MDbgProcess.Detach() call is in a finally such that if the harness does crash in the middle, then it will at least detach from the target app.

Lines of Code vs. Functionality: C# vs. Mdbg script:
You could do the same thing with an MDbg script like:
    attach %1
    for where
    detach

Where %1 is the pid of interest.
This is a cute tangent about lines of code vs. functionality:
3 lines of Mdbg script provide the raw functionality of attach, get the callstacks, and detach. Though you don't get control over formatting, and it doesn't scale well to doing things differently.
We go up to 70 lines of C# to be able to run it from a C# harness without pulling in MDbg.exe proper.
We go up to 500 lines of C# once we add dumping values, a few fancy options (attach by name), error checks, and spew to XML instead of just using the default ToString().

Sample output:

Sample output from the real XML-based tool looks like this (I removed some redundant frames).  It has an arbitrarily policy to dump the first depth of fields for reference types. I plan to publish the source for this tool somewhere on MSDN.

 
<!--Snapshot of managed process taken from SnapShot gathering tool (built on MDbg).-->
<process pid="2144">
  <thread tid="3252">
    <callstack>
      <frame hint="C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll!System.IO.TextReader.SyncTextReader.ReadLine (source line information unavailable)" il="0" mapping="MAPPING_UNMAPPED_ADDRESS">
        <locals />
        <arguments>
          <value name="this" type="System.IO.TextReader.SyncTextReader">
            <fields>
              <value name="_in" type="System.IO.StreamReader">System.IO.StreamReader</value>
              <value name="Null" type="System.IO.TextReader"><null></value>
              <value name="__identity" type="System.Object"><null></value>
            </fields>
          </value>
        </arguments>
      </frame>
      <frame hint="C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll!System.Console.ReadLine (source line information unavailable)" il="0" mapping="MAPPING_EPILOG">
        <locals />
        <arguments />
      </frame>
      <frame hint="C:\bugs\hello.exe!t.Main (hello.cs:133)" il="273">
        <locals>
          <value name="CS$1$0000" type="System.Int32">0</value>
          <value name="CS$4$0001" type="System.Boolean">False</value>
          <value name="x" type="System.Int32">3</value>
          <value name="t2" type="t">
            <fields>
              <value name="MyString" type="System.String">"hi!"</value>
              <value name="m_x" type="System.Int32">0</value>
            </fields>
          </value>
          <value name="tInt" type="System.RuntimeType">
            <fields>
              <value name="m_cache" type="System.IntPtr">0</value>
              <value name="m_handle" type="System.RuntimeTypeHandle">System.RuntimeTypeHandle</value>
              <value name="s_typeCache" type="System.RuntimeType.TypeCacheQueue"><null></value>
              <value name="s_typedRef" type="System.RuntimeType">System.RuntimeType</value>
              <value name="s_ActivatorCache" type="System.RuntimeType.ActivatorCache"><null></value>
              <value name="s_ForwardCallBinder" type="System.OleAutBinder"><null></value>
              <value name="FilterAttribute" type="System.Reflection.MemberFilter"><null></value>
              <value name="FilterName" type="System.Reflection.MemberFilter"><null></value>
              <value name="FilterNameIgnoreCase" type="System.Reflection.MemberFilter"><null></value>
              <value name="Missing" type="System.Object"><null></value>
              <value name="Delimiter" type="System.Char">\0</value>
              <value name="EmptyTypes" type="System.Type[]"><null></value>
              <value name="defaultBinder" type="System.Object"><null></value>
              <value name="valueType" type="System.Type"><null></value>
              <value name="enumType" type="System.Type"><null></value>
              <value name="objectType" type="System.Type"><null></value>
              <value name="m_cachedData" type="System.Reflection.Cache.InternalCache"><null></value>
            </fields>
          </value>
          <value name="fp1" type="t.FP1">
            <fields>
              <value name="_invocationList" type="System.Object"><null></value>
              <value name="_invocationCount" type="System.IntPtr">0</value>
              <value name="_target" type="t.FP1">t.FP1</value>
              <value name="_methodBase" type="System.Reflection.MethodBase"><null></value>
              <value name="_methodPtr" type="System.IntPtr">3416108</value>
              <value name="_methodPtrAux" type="System.IntPtr">9515312</value>
            </fields>
          </value>
          <value name="q" type="t[]">array [2]</value>
          <value name="s1" type="System.String">"abc"</value>
          <value name="s2" type="System.String">"abc"</value>
        </locals>
        <arguments>
          <value name="args" type="System.String[]">array [1]</value>
        </arguments>
      </frame>
    </callstack>
  </thread>
</process>