Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Attaching a debugger to code running in another app domain programmatically

Tags:

c#

debugging

vsix

I am working on a Visual Studio extension and one of it's functions creates a new app domain and load an assembly into that app domain. Then it runs some functions in the app domain. What I'd like to do, and am not sure if it's possible, is have my extension attach a debugger to the code running in the new app domain so when that code fails, I can actually see what's going on. Right now I'm flying blind and debugging the dynamical loaded assembly is a pain.

So I have a class that creates my app domain something like this:

domain = AppDomain.CreateDomain("Test_AppDomain", 
    AppDomain.CurrentDomain.Evidence, 
    AppDomain.CurrentDomain.SetupInformation);

And then creates an object like this:

myCollection = domain.CreateInstanceAndUnwrap(
            typeof(MyCollection).Assembly.FullName,
            typeof(MyCollection).FullName,
            false,
            BindingFlags.Default,
            null,
            new object[] { assemblyPath }, null, null);

MyCollection does something like this in it's constructor:

_assembly = Assembly.LoadFrom(assemblyPath);

So now that assembly has been loaded into Test_AppDomain since the MyCollection object was created in that domain. And it's that loaded assembly that I need to be able to attach the debugger to.

At some point myCollection creates an instance of an object and hooks up some events:

currentObject = Activator.CreateInstance(objectType) as IObjectBase;
proxy.RunRequested += (o, e) => { currentObject?.Run(); };

And basically where I have the handler for RunRequested and it runs currentObject?.Run(), I want to have a debugger attached, although it probably wouldn't be a problem (and may actually work better) if the debugger was attached earlier.

So is there a way to achieve this? Is it possible to programmatically attach a debugger when the user triggers the event that will lead to the Run function of the object created in the new AppDomain being called? How do I get the debugger attached to that (and not the extension itself)?

I tried something like this:

var processes = dte.Debugger.LocalProcesses.Cast<EnvDTE.Process>();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess().Id;
var process = processes.FirstOrDefault(p => p.ProcessID == currentProcess);
process?.Attach();

But it seems the id from System.Diagnostics.Process.GetCurrentProcess().Id doesn't exist within LocalProcesses?

like image 243
Matt Burland Avatar asked Oct 05 '16 15:10

Matt Burland


People also ask

What is the shortcut to step over a method when debugging?

F5 : Step into method. F6 : Step over line. F7 : Step out of method.


1 Answers

Even though you likely have already moved on, I found the problem very fascinating (and related to what I've been researching to blog about). So I gave it a shot as an experiment - I wasn't sure how you intended to trigger the Run() method with events (and even if it was material for your use case) so I opted for a simple method call.

Injecting Debugger.Launch()

as a PoC I ended up IL-emitting a derived class and injecting a debugger launch call before passing it onto dynamically loaded method:

public static object CreateWrapper(Type ServiceType, MethodInfo baseMethod)
{
    var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"newAssembly_{Guid.NewGuid()}"), AssemblyBuilderAccess.Run);
    var module = asmBuilder.DefineDynamicModule($"DynamicAssembly_{Guid.NewGuid()}");
    var typeBuilder = module.DefineType($"DynamicType_{Guid.NewGuid()}", TypeAttributes.Public, ServiceType);
    var methodBuilder = typeBuilder.DefineMethod("Run", MethodAttributes.Public | MethodAttributes.NewSlot);

    var ilGenerator = methodBuilder.GetILGenerator();

    ilGenerator.EmitCall(OpCodes.Call, typeof(Debugger).GetMethod("Launch", BindingFlags.Static | BindingFlags.Public), null);
    ilGenerator.Emit(OpCodes.Pop);

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.EmitCall(OpCodes.Call, baseMethod, null);
    ilGenerator.Emit(OpCodes.Ret);

    /*
     * the generated method would be roughly equivalent to:
     * new void Run()
     * {
     *   Debugger.Launch();
     *   base.Run();
     * }
     */

    var wrapperType = typeBuilder.CreateType();
    return Activator.CreateInstance(wrapperType);
}

Triggering the method

Creating a wrapper for loaded method seems to be as easy as defining a dynamic type and picking a correct method from target class:

var wrappedInstance = DebuggerWrapperGenerator.CreateWrapper(ServiceType, ServiceType.GetMethod("Run"));
wrappedInstance.GetType().GetMethod("Run")?.Invoke(wrappedInstance, null);

Moving on to AppDomain

The above bits of code don't seem to care much for where the code will run, but when experimenting I discovered that I'm able to ensure the code is in correct AppDomain by either leveraging .DoCallBack() or making sure that my Launcher helper is created with .CreateInstanceAndUnwrap():

public class Program
{
    const string PathToDll = @"..\..\..\ClassLibrary1\bin\Debug\ClassLibrary1.dll";

    static void Main(string[] args)
    {
        var appDomain = AppDomain.CreateDomain("AppDomainInMain", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation);

        appDomain.DoCallBack(() =>
        {
            var launcher = new Launcher(PathToDll);
            launcher.Run();
        });
    }
}
public class Program
{
    const string PathToDll = @"..\..\..\ClassLibrary1\bin\Debug\ClassLibrary1.dll";

    static void Main(string[] args)
    {
        Launcher.RunInNewAppDomain(PathToDll);
    }
}
public class Launcher : MarshalByRefObject
{
    private Type ServiceType { get; }

    public Launcher(string pathToDll)
    {
        var assembly = Assembly.LoadFrom(pathToDll);
        ServiceType = assembly.GetTypes().SingleOrDefault(t => t.Name == "Class1");
    }

    public void Run()
    {
        var wrappedInstance = DebuggerWrapperGenerator.CreateWrapper(ServiceType, ServiceType.GetMethod("Run"));
        wrappedInstance.GetType().GetMethod("Run")?.Invoke(wrappedInstance, null);
    }

    public static void RunInNewAppDomain(string pathToDll)
    {
        var appDomain = AppDomain.CreateDomain("AppDomainInLauncher", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation);

        var launcher = appDomain.CreateInstanceAndUnwrap(typeof(Launcher).Assembly.FullName, typeof(Launcher).FullName, false, BindingFlags.Public|BindingFlags.Instance,
            null, new object[] { pathToDll }, CultureInfo.CurrentCulture, null);
        (launcher as Launcher)?.Run();

    }
}
like image 157
timur Avatar answered Oct 24 '22 01:10

timur