//-----------------------------------------------------------------------------
// 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.
Comments
Anonymous
August 31, 2005
I hear IronPython is a great managed scripting language to embed in other managed apps, so I thought...
Anonymous
September 01, 2005
Previously, I added IronPython scripting support to a real existing application, MDbg (a managed debugger...
Anonymous
September 02, 2005
Mike Stall recently completed a project to embed IronPython into the MDbg debugger as an MDbg extension.&nbsp;...
Anonymous
September 02, 2005
Mike Stall recently completed a project to embed IronPython into the MDbg debugger as an MDbg extension.&nbsp;...
Anonymous
September 02, 2005
Mike Stall recently completed a project to embed IronPython into the MDbg debugger as an MDbg extension.&nbsp;...
Anonymous
September 02, 2005
Mike Stall recently completed a project to embed IronPython into the MDbg debugger as an MDbg extension.&nbsp;...