Sample code for MDbg-IronPython extension

 //-----------------------------------------------------------------------------
// Simple extension to hookup Iron Python to Mdbg
//
// Mike Stall:  https://blogs.msdn.com/jmstall 
// IronPython from here: https://workspaces.gotdotnet.com/ironpython   
// Mdbg here: https://blogs.msdn.com/jmstall/archive/2005/11/08/mdbg_linkfest.aspx 
// 
// This targets the IronPython Beta 1 release (which has breaking changes from .0.9.3)
//
// To use this extension, you must build it as a dll, and then load it
// into mdbg via the "Load" command. 
//
// This requires a reference to the usual culprit of Mdbg extension dlls, 
// as well as to IronPython.dll
//
// You can build this in Visual Studio 2005 by adding a new Class Library project
// to the Mdbg sample, and then adding the proper references. 
//-----------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;

using Microsoft.Samples.Tools.Mdbg;
using Microsoft.Samples.Debugging.MdbgEngine;
using Microsoft.Samples.Debugging.CorDebug;

using System.Text.RegularExpressions;
using System.Globalization;
using System.IO;
using SS = System.Diagnostics.SymbolStore;


// extension class name must have [MDbgExtensionEntryPointClass] attribute on 
// it and implement a LoadExtension()
[MDbgExtensionEntryPointClass(
    Url = "https://blogs.msdn.com/jmstall",
    ShortDescription = "Eventing test extension."
)]
public class PythonExt : CommandBase
{
    // Adapter to expose some methods to Python
    // avoid static methods.
    class Util
    {
        public void ExecuteCommand(string arg)
        {
            CommandBase.ExecuteCommand(arg);
        }
    }

    // This is called when the python extension is first loaded.
    public static void LoadExtension()
    {
        MDbgAttributeDefinedCommand.AddCommandsFromType(Shell.Commands, typeof(PythonExt));
        WriteOutput("IronPython-Mdbg Extension loaded");

        m_python = new IronPython.Hosting.PythonEngine();

        // Set =true to avoid having IronPython output files for everything it compiles.
        //IronPython.AST.Options.DoNotSaveBinaries = true;                

        // Add the current directory to the python engine search path.
        // @todo - could add the symbol path Debugger.Options.SymbolPath here too.
        m_python.AddToPath(Environment.CurrentDirectory);


        // Tell Python about some key objects in Mdbg. Python can then reflect over these objects.
        // These variables live at some special "main" scope. Python Modules imported via python "import" command
        // can't access them. Use PythonEngine.ExecuteFile() to import files such that they can access these vars.
        WriteOutput("Adding variable 'Shell' to python main scope");
        m_python.SetVariable("Shell", Shell);
        m_python.SetVariable("MDbgUtil", new Util());

        // See MyResolveHandler for reasons why we hook this.
        // Only need this for "import" command.
        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveHandler);

        // Hook console. The Python Console refers to the pysical input + output from the python UI and
        // is separate from output of actual python commands (like "print").         
        // We need this to enter interactive mode.
        m_python.MyConsole = new MyMdbgConsole();

        // Hook input + output. This is redirecting pythons 'sys.stdout, sys.stdin, sys.stderr' 
        // This connects python's sys.stdout --> Stream --> Mdbg console.
        Stream s = new MyStream();
        
#if false 
        // This is for versions before .0.9.3                 
        // IronPython.Modules.sys.stdin = 
        //  IronPython.Modules.sys.stderr = 
        //   IronPython.Modules.sys.stdout = new IronPython.Objects.PythonFile(s, "w", false);        
#elif false
        // 0.9.3. breaks the above line because it adds a "name" string parameter. Here's what it should be in 0.9.3:
        //IronPython.Objects.Ops.sys.stdin = new IronPython.Objects.PythonFile(s, "stdin", "r", false); 
        //IronPython.Objects.Ops.sys.stderr = new IronPython.Objects.PythonFile(s, "stderr", "w", false); 
        //IronPython.Objects.Ops.sys.stdout = new IronPython.Objects.PythonFile(s, "stdout", "w", false);
#else        
        // Beta 1 breaks the above again. IMO, this integrates into .NET much cleaner:
        m_python.SetStderr(s);
        m_python.SetStdin(s);
        m_python.SetStdout(s);
#endif
    }

    // Stream to send Python Output to Mdbg console.
    // This can be used to redirect python's sys.stdout.
    class MyStream : Stream
    {
        #region unsupported Read + Seek members
        public override bool CanRead
        {
            get { return false; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override void Flush()
        {
            // nop
        }

        public override long Length
        {
            get { throw new NotSupportedException("Seek not supported"); } // can't seek 
        }

        public override long Position
        {
            get
            {
                throw new NotSupportedException("Seek not supported");  // can't seek 
            }
            set
            {
                throw new NotSupportedException("Seek not supported");  // can't seek 
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException("Reed not supported"); // can't read
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException("Seek not supported"); // can't seek
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException("Seek not supported"); // can't seek
        }
        #endregion

        public override void Write(byte[] buffer, int offset, int count)
        {
            // Very bad hack: Ignore single newline char. This is because we expect the newline is following
            // previous content and we already placed a newline on that.
            if (count == 1 && buffer[offset] == '\n')
                return;
            // Code update from ShawnFa to fix case for '\r'
            StringBuilder sb = new StringBuilder();
            while (count > 0)
            {
                char ch = (char)buffer[offset]; if (ch == '\n')
                {
                    Shell.IO.WriteOutput("STDOUT", sb.ToString());
                    sb.Length = 0; // reset.                
                }
                else if (ch != '\r')
                {
                    sb.Append(ch);
                }
                offset++;
                count--;
            }

            // Dump remainder. @todo - need some sort of "Write" to avoid adding extra newline.
            if (sb.Length > 0)
                Shell.IO.WriteOutput("STDOUT", sb.ToString());
        }
    }

    // Console hook to redirect IronPython output to MDbg's console.
    // This allows us to enter PythonInteractive mode.
    // This is very important if MDbg's console is redirected (eg, to a GUI)
    class MyMdbgConsole : IronPython.Hosting.IConsole
    {
        #region IConsole Members

        // The keyword they type in to exit the console. 
        // They enter via "Pyin", so "Pyout" seems a good choice. Don't want to pick something like "exit"
        // that would cause Mdbg to exit if accidentally used at the Mdbg prompt.
        public static readonly string ExitKeyword = "pyout";

        // Sends the 'end-of-file' via returning null.
        public string ReadLine()
        {
            string text;
            Shell.IO.ReadCommand(out text);

            // If they type the exit keyword, we'll translate that to EOF and
            // back out of the python interactive shell.
            if (text == ExitKeyword)
            {
                text = null;
            }
            return text;
        }

        // !!! Oh no!! How do we map Write() when we only have WriteLine()?
        // Queue all Writes and then flush them? Still doesn't account for styles.
        public void Write(string text, IronPython.Hosting.Style style)
        {
            WriteLine(text, style);
        }

        public void WriteLine(string text, IronPython.Hosting.Style style)
        {
            WriteOutput(text);
        }

        // @todo - Map from IronPython.Hosting.Style --> Mdbg console.        
        #endregion
    }

    // If Python Options.DoNotSaveBinaries=false (the default), then it will try to load the newly
    // generated assembly using LoadFrom(), and likely fail to find resolve the references.
    // Need this hack to cover that up.
    // @todo - there has to be a better way...
    static Assembly MyResolveHandler(object sender, ResolveEventArgs args)
    {
        string name = args.Name;
        string[] x = name.Split(',');
        string n = x[0];
        if (n == "IronPython")
        {
            Type t = typeof(IronPython.Hosting.PythonEngine);
            return t.Assembly;
        }
        return null;
    }

    // The main python engine.
    static IronPython.Hosting.PythonEngine m_python;


    // Command to import python modules into the same scope that we called SetVariable
    // on during initialization. This lets these modules access the key Mdbg vars
    // and thus traverse the Mdbg tree.
    // You can reload a file by just reimporting it.
    //
    // args is the filename to load
    [
        CommandDescription(
        CommandName = "pimport",
        MinimumAbbrev = 2,
        ShortHelp = "Import python script",
        LongHelp = @"
Imports a python script into the evaluation scope so that it can access Mdbg vars.
You can reload a file by importing it multiple times.
"
        )
    ]
    public static void PythonImport(string args)
    {
        m_python.ExecuteFile(args);
    }

    // Enter Python-interactive mode.
    // This drops us to a python prompt:
    // - so we don't need to use the "python" mdbg command to execute python commands). 
    // - It also lets us type multi-line python commands (like defining functions).    
    [
        CommandDescription(
        CommandName = "pyinteractive",
        MinimumAbbrev = 4,
        ShortHelp = "Go into python interactive mode",
        LongHelp = "Enter python interactive mode, like what you'd have in a python shell"
        )
    ]
    public static void PythonInteractive(string args)
    {
        WriteOutput("Entering Python Interactive prompt.");
        WriteOutput("Type '" + MyMdbgConsole.ExitKeyword + "' leave PythonInteractive mode and get back to Mdbg prompt.");

        // This will call back via our user supplied IConsole interface (MyMdbgConsole class).
        // It keep fetching commands via IConsole.ReadLine() and not return until that yields null.
        m_python.RunInteractive();

        WriteOutput("Leaving Python Interactive prompt.");
    }


    // Execute a python command. Args is the command to execute.
    // Commands can include expressions as well as defining functions.
    [
        CommandDescription(
        CommandName = "python",
        MinimumAbbrev = 2,
        ShortHelp = "Execute a single python command",
        LongHelp =
@"Execute a single python command or expression.
Example: 
    py 1+2
    py def MyAdd(a,b): return a + b
"
        )
    ]
    public static void PythonCommand(string args)
    {
        // Executes 1 Python command.
        m_python.Execute(args);
    }

    // Eval a python expression. Args is the command to execute.
    [
        CommandDescription(
        CommandName = "peval",
        MinimumAbbrev = 2,
        ShortHelp = "Eval python expression",
        LongHelp =
@"Execute a single python expression."
        )
    ]
    public static void PyEval(string args)
    {
        // Executes 1 Python command.
        object o = m_python.Evaluate(args);
        PrintPythonResult(o);
    }


    // Helper to print a result
    static void PrintPythonResult(object o)
    {
        if (o == null)
        {
            WriteOutput("Result:null");
        }
        else
        {
            WriteOutput("Result:" + o.ToString() + " (of type=" + o.GetType() + ")");
        }
    }

    #region Python Conditional Breakpoint

    // Conditional breakpoint which executes an IronPython expression when hit.
    class PythonBreakpoint : MDbgFunctionBreakpoint
    {
        public PythonBreakpoint(string pythonCommand, MDbgBreakpointCollection breakpointCollection, ISequencePointResolver location)
            : base(breakpointCollection, location)
        {
            m_pythonCommand = pythonCommand;
        }
        string m_pythonCommand;

        // Return null to continue
        // Return non-null for a stop-reason to stop the shell.
        public override object OnHitHandler(CustomBreakpointEventArgs e)
        {
            WriteOutput("Python BP hit:" + m_pythonCommand);

            {
                // Compensate for MDbg bug. Need to refresh stack because we're in a callback.
                Debugger.Processes.Active.Threads.RefreshStack();
            }

            object o = null;

            try
            {
                // Execute the python expression to determine if we should stop or not.
                o = m_python.Evaluate(m_pythonCommand);
                PrintPythonResult(o);
                if (o == null)
                {
                    WriteOutput("Null stop reason - continuing process from breakpoint.");
                }
                if (o is bool)
                {
                    // Special case common scenario for boolean expressions
                    if ((bool)o)
                    {
                        WriteOutput("Expression is true. Stopping at breakpoint");
                        return true;
                    }
                    else
                    {
                        WriteOutput("Expression is false. continuing");
                        return null;
                    }
                }
                else
                {
                    WriteOutput("Non-null stop reason. Halting process at breakpoint.");
                }
                return o;
            }
            catch (System.Exception ex)
            {
                o = "Exception thrown:" + ex.Message;
                WriteOutput("Exception thrown. Stopping at breakpoint.");
                return o;
            }
        }
        public override string ToString()
        {
            return base.ToString() + "(python:" + m_pythonCommand + ")";
        }
    }


    [
    CommandDescription(
    CommandName = "pyb",
    MinimumAbbrev = 3,
    ShortHelp = "Conditional Python Breakpoint",
    LongHelp =
@"Usage: pyb <breakpoint args> '|' <python expression>
Creates a conditional breakpoint at the given location. 
When the BP is hit, the python expression is evaluated for a stop-reason.
Iff it is null or False, the bp continues. 
Breakpoint syntax is the same as the 'break' command.
example:
    pyb 23 | func(1)
"
    )
    ]
    public static void PythonBreakpointCmd(string args)
    {
        // We're adding a breakpoint. Parse the argument string.
        MDbgProcess p = Debugger.Processes.Active;
        MDbgBreakpointCollection breakpoints = p.Breakpoints;

        // Use a bar '|' to split breakpoint location from python string. BP loc comes
        // first because it doesn't contain a bar, so that simplifies parsing. Everything
        // after the bar is a python expression.
        int idx = args.IndexOf('|');
        if (idx < 0)
        {
            throw new ArgumentException("Expected '|' to separate python expression from breakpoint location.");
        }
        string stLoc = args.Substring(0, idx - 1);
        string stPythongExpression = args.Substring(idx + 1);
        ISequencePointResolver bploc = Shell.BreakpointParser.ParseFunctionBreakpoint(stLoc);
        if (bploc == null)
        {
            throw new Exception("Don't understand the syntax");
        }

        PythonBreakpoint bp = new PythonBreakpoint(stPythongExpression, breakpoints, bploc);
        WriteOutput(bp.ToString());
    }
    #endregion Python Conditional Breakpoint

} // end class for my extensions.