Attaching a debugger to code running in another app domain programmatically
Asked Answered
C

2

16

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?

Cyanohydrin answered 5/10, 2016 at 15:2 Comment(9)
do you have access to the codebase of the assembly you are trying to load ? If so you could put this in the beginning of the Run method ` #if DEBUG System.Diagnostics.Debugger.Launch(); #endif`Government
@Vignesh.N: I do, but there are currently 500+ assemblies and I'd prefer not to have to go edit every one of them. Although that might be something I can stick into the common base class that they all use. However, I only want the debugger while testing with my extension. When I actually deploy these (they are internal) I like to keep them in the debug build so when they break I have all the debug info available. Still, that's a good idea...Cyanohydrin
Did you add the pdb file as explained in msdn.microsoft.com/en-us/library/x54fht41(v=vs.100).aspx ?Neogothic
@Simon: The pdb files exist in the same folder as the assembly being loadingCyanohydrin
Yes, but for a different AppDomain, it may not be picked up. Check if they are registered when actually debugging the code. If not, add it at that moment (while debugging)Neogothic
Hmm. I think it's probably not possible for the Visual Studio debugger to attach to its own process. When you develop an extension and start it in debug mode, VS will fire up a separate instance of VS with the extension loaded and the original VS instance debugger attached. I'm also under the impression that the .NET debugger attaches to a process rather than a specific AppDomain within a process, but I might be mistaken.Friction
When a program that was started outside of any development environment crashes due to uncaught exception, and at least one version of Visual Studio is installed on the machine, Windows will halt the crashing process and display a dialog that gives the user the option to start an instance of Visual Studio to debug the process. Maybe you want to do something similar, i.e. have your extension start a separate instance of VS and tell it to attach to the original VS where the AppDomain with the dynamically loaded dll is...Friction
You'd have to make sure that the new instance of VS doesn't automatically fire up yet another VS in an infinte loop, though :-) But maybe that AppDomain and dynamic assembly only happens after the user performs some specfic action? Unfortunately, I have no idea how to lauch a separate VS and tell it to attach to the current process, but since Windows can do this (or maybe it's the .NET runtime that does it?), I have a feeling that it would be possible.Friction
@Government The code I was attempting to debug was part of another solution - added System.Diagnostics.Debugger.Break() and it launched the code file into my running VS instance - from there I could set breakpoints on anything. #if DEBUG System.Diagnostics.Debugger.Launch(); System.Diagnostics.Debugger.Break(); #endifExperienced
P
1

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();

    }
}
Paralyze answered 28/11, 2020 at 10:19 Comment(0)
G
0

One way to get around this is to generate another assembly with a function that will take in a MethodInfo object and simply call System.Diagnostics.Debugger.Launch() and then the given MethodInfo, and then all you have to do is unwrap that assembly's function call it with whatever Method's Info you want to start the actual domain in, and your good it will enable the debugger and then call the method you want it to start in.

Gwenore answered 14/2, 2020 at 23:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.