Sample code for snapshot tool

 
//-----------------------------------------------------------------------------
// Harness to snapshot a process's callstacks and some variables.
// Built on MDbg.
// Needs a reference to MdbgCore.dll (ships in CLR 2.0 SDK).
//
// Author: Mike Stall (https://blogs.msdn.com/jmstall)
// More info: https://blogs.msdn.com/jmstall/archive/2005/11/28/snapshot.aspx 
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Microsoft.Samples.Debugging.MdbgEngine;
using System.IO;
using System.Xml;
using Microsoft.Samples.Debugging.CorDebug.NativeApi;
using System.Text.RegularExpressions;

namespace Snapshot
{
    class Program
    {
        // Helper to get options.
        // this may also do some work to resolve options (and so it's not just a trivial container).
        // The options handed back are not necessarily verified to be valid (eg, pid may be bogus).
        class Options
        {
            // Print the usage mesasge
            static void PrintUsage()
            {
                Console.WriteLine("Snapshot - takes a snapshot (callstacks w/ locals on all threads) and");
                Console.WriteLine(" writes out to an XML file. This will do an INVASIVE attach (attaches");
                Console.WriteLine(" as a managed debugger, does the inspection, and then detaches)");
                Console.WriteLine(" Parameters:");
                Console.WriteLine("   -out:<filename>    | specify filename of output xml file. (Default:out.xml)");
                Console.WriteLine("   -global:<var>      | captures global variable. Use C# syntax");
                Console.WriteLine("                      | eg, 'module.exe!Class.StaticField'");
                Console.WriteLine("   -pid:<pid>         | attach to the given pid.");
                Console.WriteLine("   -name:<name>       | attach to app with the given shortname");
                Console.WriteLine("   -?                 | prints this message.");

            }

            // Helper to parse an option string like "-option:value" and get the parts from it.
            static void GetParts(string arg, out string option, out string value)
            {
                Regex regex = new Regex(@"[-/](.+?):(.+)", RegexOptions.Singleline);

                Match m = regex.Match(arg);
                if (!m.Success)
                {
                    throw new OptionException("Illegal argument:" + arg);
                }

                option = m.Groups[1].Value;
                value = m.Groups[2].Value;
            }

            // Constructor
            // Creates an Options class by parsing command-line options.
            public Options(string[] args)
            {
                try
                {
                    Worker(args);

                    // Set defaults
                    if (m_outFile == null)
                    {
                        m_outFile = "out.xml";
                    }
                }
                catch (System.ApplicationException e)
                {
                    // Illegal option. Print error now and exit.
                    Console.WriteLine();

                    ConsoleColor c = Console.ForegroundColor;
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine(e.Message);
                    Console.ForegroundColor = c;

                    Console.WriteLine();
                    PrintUsage();
                    Environment.Exit(1);
                }
            }

            // Do the real work of parsing through the options.
            // Throws an ApplicationException on any errors.
            void Worker(string[] args)
            {
                if ((args == null) || (args.Length == 0))
                {
                    throw new OptionException("Print help"); // print help                
                }

                foreach (string arg in args)
                {
                    if (arg == "-?" || arg == "/?")
                    {
                        throw new OptionException("User requested help.");
                    }

                    string option;
                    string value;

                    GetParts(arg, out option, out value);

                    switch (option)
                    {
                        case "out":
                            if (m_outFile != null)
                            {
                                throw new OptionException("Can't use '-out' twice. First value was '" + m_outFile + "'. Can't set to '" + value + "'");
                            }
                            m_outFile = value;
                            break;

                        case "global":
                            m_globals.Add(value);
                            break;

                        case "pid":
                            try
                            {
                                AssignPid(int.Parse(value, System.Globalization.NumberStyles.AllowHexSpecifier));
                            }
                            catch (FormatException)
                            {
                                throw new OptionException("Pids must be a decimal or hex number. Value '" + value + "' is an illegal pid.");
                            }
                            break;
                        case "name":
                            AssignPidByName(value);
                            break;

                        default:
                            throw new OptionException("Unrecognized option:" + option);
                    }
                }
            }

            // Assign the pid based off the method name.
            void AssignPidByName(string value)
            {
                // Given the command line args, determine the target pid.
                // This could look by friendly name or by exact pid match (decimal or hex).

                // Chop off extension to get short name.
                Regex r = new Regex(@"\.exe");
                string shortName = r.Replace(value, "", 1);

                // find by name
                Process[] list = Process.GetProcessesByName(shortName); // takes short name
                if (list.Length == 0)
                {
                    throw new OptionException("No process of name '" + value + "'");
                }
                if (list.Length > 1)
                {
                    throw new OptionException("Multiple processes of name '" + value + "'. Use -pid to disambiguate.");
                }
                Process p = list[0];
                int pid = p.Id;


                AssignPid(pid);
            }

            // Assign the pid and check for uniqueness
            void AssignPid(int pid)
            {
                if (m_pid != 0)
                {
                    throw new OptionException("Can't attach to multiple targets (can only use -pid or -name once).");
                }
                m_pid = pid;
            }

            #region Properties
            // the XML file to dump all the information to.
            public string OutputXmlFile
            {
                get { return m_outFile; }
            }
            string m_outFile;

            // A list of globals to capture. Never null (though it may be an enumerator with 0 items).
            public IList<string> Globals
            {
                get { return m_globals; }
            }
            List<string> m_globals = new List<string>();

            // The Pid to attach the harness to.
            public int Pid
            {
                get { return m_pid; }
            }
            int m_pid;
            #endregion Properties

            // Don't use ApplicationExceptionDirectly to avoid violating:
            // https://www.gotdotnet.com/team/fxcop/docs/rules.aspx?version=1.32&url=/Usage/DoNotRaiseReservedExceptionTypes.html 
            class OptionException : ApplicationException
            {
                public OptionException(string message)
                    :
                    base(message)
                {
                }
            }

        } // end Options Class


        static void Main(string[] args)
        {
            // Need to determine which process
            Options opt = new Options(args);

            Console.WriteLine("Debugging process pid={0}", opt.Pid);

            MDbgEngine debugger = new MDbgEngine();

            // Get a Text Writer to spew the PDB to.
            XmlDocument doc = new XmlDocument();
            XmlWriter xw = doc.CreateNavigator().AppendChild();

            xw.WriteStartDocument();
            xw.WriteComment("Snapshot of managed process taken from SnapShot gathering tool (built on MDbg).");
            {
                xw.WriteStartElement("process");
                xw.WriteAttributeString("pid", opt.Pid.ToString());

                MDbgProcess proc = null;
                try
                {
                    proc = debugger.Attach(opt.Pid);
                    DrainAttachEvents(debugger, proc);

                    // Dump custom global data.
                    foreach (string global in opt.Globals)
                    {
                        DumpGlobalValue(proc, global, xw);
                    }

                    DumpAllThreads(proc, xw);

                }
                finally
                {
                    // We always want to detach from target.
                    if (proc != null)
                    {
                        proc.Detach().WaitOne();
                    }
                }
            }
            xw.WriteEndDocument();
            xw.Close();

            doc.Save(opt.OutputXmlFile);

            Console.WriteLine("Done with detach");
        }

        #region Find Hack Frame
        // This is an evil hack to work around a bug in the MDbg layer.
        // MDbgProcess.ResolveVariable needs a non-null MDbgFrame object that it can resolve vars on.
        // This is conceptually not needed to resolve globals (which is what we're looking for).
        // So we search through and find a usable frame.
        private static MDbgFrame FindHackFrameWorker(MDbgProcess proc)
        {
            foreach (MDbgThread t in proc.Threads)
            {
                foreach (MDbgFrame f in t.Frames)
                {
                    try
                    {
                        // Throws an exception if invalid.
                        f.Function.GetArguments(f);

                        // Frame can be used to resolve variables. Done with search.
                        return f;
                    }
                    catch
                    {
                    }
                }
            }
            return null;
        }

        // Cache result of FindHackFrameWorker
        private static MDbgFrame FindHackFrame(MDbgProcess proc)
        {
            if (m_cachedHackFrameValid != proc)
            {
                m_cachedHackFrame = FindHackFrameWorker(proc);
                m_cachedHackFrameValid = proc;
            }
            return m_cachedHackFrame;
        }
        static MDbgProcess m_cachedHackFrameValid;
        static MDbgFrame m_cachedHackFrame;

        #endregion Find Hack Frame

        #region Dump to XML
        // dump value of custom globals.
        private static void DumpGlobalValue(MDbgProcess proc, string globalValue, XmlWriter xw)
        {
            // This is an insane hack
            MDbgFrame f = FindHackFrame(proc);
            if (f == null)
            {
                xw.WriteComment("Can't find resolution frame for global:" + globalValue);
                return;
            }

            MDbgValue v = proc.ResolveVariable(globalValue, f);
            if (v == null)
            {
                xw.WriteComment("Can't resolve global:" + globalValue);
            }
            else
            {
                xw.WriteStartElement("global");
                DumpValue(v, xw);
                xw.WriteEndElement(); // "global";
            }
        }

        // Dump all callstacks on all threads. For each callstack, dump all locals + parameters.
        static void DumpAllThreads(MDbgProcess proc, XmlWriter xw)
        {
            MDbgThreadCollection tc = proc.Threads;
            foreach (MDbgThread t in tc)
            {
                xw.WriteStartElement("thread");
                xw.WriteAttributeString("tid", t.Id.ToString());

                xw.WriteStartElement("callstack");
                foreach (MDbgFrame f in t.Frames)
                {
                    DumpFrame(f, xw);
                }
                xw.WriteEndElement(); // callstack

                xw.WriteEndElement(); // thread
            }
        }

        // Dump a single frame.
        static void DumpFrame(MDbgFrame f, XmlWriter xw)
        {
            // Don't need "Information Only" Frames (ICorDebugInternalFrame).
            if (f.IsInfoOnly)
            {
                return;
            }

            try
            {
                xw.WriteStartElement("frame");
                {
                    // This will print the frame name and other random data.
                    // This is not structured data, so we call it a "hint".
                    // We could add mroe XML writes to print this as structured data.
                    string stModule = f.Function.Module.CorModule.Name;
                    xw.WriteAttributeString("hint", stModule + '!' + f.ToString());

                    {
                        // Print IL offset.
                        uint ip;
                        CorDebugMappingResult result;

                        f.CorFrame.GetIP(out ip, out result);
                        xw.WriteAttributeString("il", ip.ToString());
                        if (result != CorDebugMappingResult.MAPPING_EXACT)
                        {
                            xw.WriteAttributeString("mapping", result.ToString());
                        }
                    }

                    // WriteLocals
                    try
                    {
                        xw.WriteStartElement("locals");
                        foreach (MDbgValue v in f.Function.GetActiveLocalVars(f))
                        {
                            DumpValue(v, xw);
                        }
                    }
                    catch
                    {
                    }
                    finally
                    {
                        xw.WriteEndElement(); // locals
                    }

                    // Write arguments                        
                    try
                    {
                        xw.WriteStartElement("arguments");
                        foreach (MDbgValue v in f.Function.GetArguments(f))
                        {
                            DumpValue(v, xw);
                        }
                    }
                    catch
                    {
                    }
                    finally
                    {
                        xw.WriteEndElement(); // arguments
                    }
                }
            }
            catch
            {
                // Swallow all errors and keep trucking.
            }
            finally
            {
                xw.WriteEndElement(); // frame
            }
        }

        // dump the Value to the xml stream.
        static void DumpValue(MDbgValue v, XmlWriter xw)
        {
            DumpValueWorker(v, 2, xw);
        }

        // Helper to dump values.
        static void DumpValueWorker(MDbgValue v, int depth, XmlWriter xw)
        {
            try
            {
                xw.WriteStartElement("value");
                xw.WriteAttributeString("name", v.Name);
                xw.WriteAttributeString("type", v.TypeName);
                {
                    bool printSummary = true;

                    if (depth > 1)
                    {
                        // Dump sub items.
                        if (v.IsComplexType)
                        {
                            xw.WriteStartElement("fields");
                            MDbgValue[] fields = v.GetFields();
                            if (fields != null)
                            {
                                foreach (MDbgValue v2 in fields)
                                {
                                    DumpValueWorker(v2, depth - 1, xw);
                                }
                            }
                            xw.WriteEndElement(); // "fields"
                            printSummary = false;
                        }
                    }

                    // If we haven't printed anything else, then print a summary
                    if (printSummary)
                    {
                        int expandDepth = 0;
                        bool fAllowFuncEval = false;
                        string val = v.GetStringValue(expandDepth, fAllowFuncEval);
                        if (val != "'\0'") // special case where WriteString breaks down on writing out '\0'. 
                        {
                            xw.WriteString(val);
                        }
                        else
                        {
                            xw.WriteString("\\0");
                        }
                    }
                }
            }
            finally
            {
                xw.WriteEndElement(); // Value
            }
        }

        #endregion Dump to XML

        #region Plumbing
        // Once you first attach to a process, you need to drain a bunch of fake startup events
        // for thread-create, module-load, etc.         
        static void DrainAttachEvents(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;
        }

        #endregion Plumbing
    }
}