Bootstrapping your Application’s AppDomainManager


Last time I mentioned that when using pure managed code to setup an AppDomainManager, you should prefer to use the environment variables rather than the registry keys.  Once you’ve decided to use the environment variables, you need to determine a strategy for setting them up.  Ideally, you’d want the environment setings to affect only your process and no other managed applications.  In order to pull that off, it’s best to set the environment variables in only your managed process.


We can easily do that by using the EnvironmentVariables collection exposed on the ProcessStartInfo class when starting off a new process.  However, using Process.Start means that we have a shim application which sets up the environment and then kicks off our real application.  Shipping two .exe’s can be somewhat ugly — and you have a problem if someone invokes your application .exe without going through the shim, so we’d like a better solution.


What if we combine the functionality of the shim application into our main application’s .exe?  In the entry point we could do a check to make sure that the AppDomainManager is setup — if it is, continue on, if not then set it up and respawn.  This actually turns out to be pretty easy to do in a few lines of code.  Here’s a sample application ADMApp, which expects to be run with CustomAppDomainManager.  Combining the shim with the application itself would produce code that looks something like this:



public static class ADMApp
{
    public static int Main(string[] args)
    {
        // determine if we need to setup an AppDomainManager, or if we’ve already done that
        // and can begin executing our application’s logic
        AppDomainManager domainManager = AppDomain.CurrentDomain.DomainManager;
        if(domainManager != null && domainManager.GetType() == typeof(CustomAppDomainManager))
        {
            // remove the .exe name from args[0]
            string[] realArguments = new string[args.Length – 1];
            for(int i = 0; i < realArguments.Length; i++)
                realArguments[i] = args[i + 1];

            return StartApplication(realArguments);
        }
        else
        {
            return SetupAppDomainManager();
        }
    }

    /// <summary>
    /// Start a new version of this process, using an AppDomainManager this time
    /// </summary>
    private static int SetupAppDomainManager()
    {
        // create a new copy of this application
        ProcessStartInfo psi = new ProcessStartInfo(
            typeof(ADMApp).Assembly.Location,
            Environment.CommandLine);

        // setup the AppDomainManager environment variables
        psi.UseShellExecute = false;
        psi.EnvironmentVariables.Add(
            “APPDOMAIN_MANAGER_ASM”,
            typeof(CustomAppDomainManager).Assembly.FullName);
        psi.EnvironmentVariables.Add(
            “APPDOMAIN_MANAGER_TYPE”,
            typeof(CustomAppDomainManager).FullName);

        // start the new copy
        Process process = Process.Start(psi);
        process.WaitForExit();
        return process.ExitCode;
    }

    /// <summary>
    /// Real entry point
    /// </summary>
    private static int StartApplication(string[] args)
    {
        // …
    }
}


Lets take a look at the code in some more detail.


In Main, the first thing we do is check to see if there’s already an AppDomainManager setup for this process.  If there is, we do a second check — to make sure that it’s the same AppDomainManager we’re expecting.  This prevents us from accidentally using somebody else’s AppDomainManager if they had already setup the environment variables or the registry keys.


If we find that either condition is not true, we call SetupAppDomainManager, which is where the real work gets done.  It starts by creating a new ProcessStartInfo that will start the .exe which we’re currently running in, passing along our command line parameters.  One issue we’ll have to deal with later is that Environment.CommandLine includes the application name as the first argument, so we’ll have to filter that out.


Once we’ve got our ProcessStartInfo, we set UseShellExecute to false, since that’s a requirement to use the EnvironmentVariables collection.  We then set the AppDomainManager variables up.  Setting these variables is another reason that this method of bootstrapping your application works nicely — you don’t have to hard code type and assembly names, the CLR will resolve those for you.


Now that we’ve setup the context for our new process to run under, we kick it off, wait for it to exit, and return its exit code.  That exit code gets propagated out of our Main, and so will return to the process that started us off in the first place.


Once the shim portion of the application kicks off the second process, we end up back in Main where we find that we do have our AppDomainManager setup properly.  Before we can start the application however, we need to strip the .exe name from args[0] (remember from above, it got there because we used Environment.CommandLine to start the second process).  Once we’ve dropped the first argument, we can invoke our application’s real Main, which is named StartApplication().  From there on, the logic of setting up the application is gone, and you can write code just as you would for any other application.


Comments (7)

  1. Leon says:

    Hi, we have a new interface in 2.0 to control the CLR: ICLRRuntimeHost. Earlier samples for CLR hosting used ICLRRuntimeHost::GetDefaultDomain/etc, but in beta 2 any AppDomain creation/manipulation is gone from that interface. How should one GetDefaultDomain/CreateDomain in .NET 2.0? I can’t get both ICLRRuntimeHost and ICorRuntimeHost. Thanks for any pointers

  2. Brien says:

    Is there any way to do this so that it’s debugger friendly? The code example prevents StartApplication from being debugged when you Run the app in the debugger since it is spawned as a new process.

    Can the shim tell the debugger to start debugging the new process?

  3. shawnfa says:

    Leon — in v2.0, you’ll need to create an AppDomainManager which interacts and creates new AppDomains. If you set the Flags property on your AppDomainManager to RegisterWithHost, then you IHostControl::SetAppDomainManager callback will get called with a pointer to your AppDomainManager object. You can then QI for an interface that you define, and control AppDomains that way.

    -Shawn

  4. shawnfa says:

    Brien — that’s good feedback. The most obvious way I can think of is to use gflags tool that comes with the Debugging Tools for Windows. You can use that tool to setup a debugger that is launched whenever an application starts. This way when the second application is started, a second debugging session will also start.

    -Shawn

  5. Brien says:

    Shawn-

    I was able to debug by throwing a Debug.Fail() into the code. It worked if I launched a new copy of DevStudio, but it caused DevStudio to hang if I tried to reuse the instance that launched the shim. It would be nice if there were some call like Debug.ConnectToParentDebugger() or the like.

    Brien

  6. shawnfa says:

    Actually debugging this is pretty easy now that I think about it. What you want to do is force the application to not relaunch itself. It will only relaunch itself if it sees that the AppDomainManager is not setup. So all you need to do is ensure that the debugger sets the environment variables before launching the app for the first time.

    You could even set the variables, then launch your debugger, which would inherit them and pass them on to the debuggee.

    -Shawn

  7. Gerade noch überlegte ich, wie man denn programmatisch eine gesignte Assembly in die GAC bekommen könnte,