Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dealing with non-serializable unhandled exceptions from a child AppDomain

We are using System.AddIn to load add-ins into separate child AppDomains, and we unload the add-in AppDomain if it has an unhandled exception.

Because the default behaviour of .NET from version 2.0 was to tear down the entire process if any AppDomain has an unhandled exception, we do this by using the "legacyUnhandledExceptionPolicy" option in our App.config and then manually tearing down the process if the unhandled exception was in the main AppDomain, or unloading the appropriate AppDomain if it was in an add-in.

This is all working great, except for one small issue: Unhandled exceptions always bubble up from the child AppDomains to the main one, and if they aren't serializable they can't successfully cross the AppDomain boundary.

Instead we get a SerializationException appearing as an UnhandledException in the main AppDomain, causing our application to tear itself down.

I can think of a few possible solutions to this problem:

  • We could not tear down the process for unhandled SerializationExceptions (yuck).

  • We could stop exceptions from propagating from the child AppDomain to the main AppDomain.

  • We could replace the unserializable exceptions with serializable ones, maybe using serialization surrogates and serialization binders. [Edit: see end for why this isn't possible using surrogates]

However the first one is pretty horrible, and I've been unsuccessful in figuring out how to do either of the other options with cross-AppDomain remoting.

Can anyone offer some advice? Any help is appreciated.

To reproduce create a console application with the following App.config:

<?xml version="1.0"?>
<configuration>
  <runtime>
    <legacyUnhandledExceptionPolicy enabled="true"/>
  </runtime>
</configuration>

And the following code:

class Program
{
    private class NonSerializableException : Exception
    {
    }

    static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += MainDomain_UnhandledException;

        AppDomain childAppDomain = AppDomain.CreateDomain("Child");
        childAppDomain.UnhandledException += ChildAppDomain_UnhandledException;

        childAppDomain.DoCallBack(
            () => new Thread(delegate() { throw new NonSerializableException(); }).Start());

        Console.ReadLine();
    }

    static void MainDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Console.WriteLine("Main AppDomain Unhandled Exception: " + e.ExceptionObject);
        Console.WriteLine();
    }

    static void ChildAppDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Console.WriteLine("Child AppDomain Unhandled Exception: " + e.ExceptionObject);
        Console.WriteLine();
    }
}



EDIT: I used reflector to see if there was any way to gain access to the BinaryFormatter used in cross-AppDomain remoting, but it ends up in this code inside the CrossAppDomainSerializer class:

internal static void SerializeObject(object obj, MemoryStream stm)
{
    BinaryFormatter formatter = new BinaryFormatter();
    RemotingSurrogateSelector selector = new RemotingSurrogateSelector();
    formatter.SurrogateSelector = selector;
    formatter.Context = new StreamingContext(StreamingContextStates.CrossAppDomain);
    formatter.Serialize(stm, obj, null, false);
}

So it creates the formatter locally in the method, and there is clearly no way to attach my own surrogate... I think this makes any more efforts in that direction futile.

like image 713
James Thurley Avatar asked Mar 28 '11 18:03

James Thurley


1 Answers

I would handle the exceptions in the remote application domain. First I would create a new assembly with the exception handling code and then I would load it into child application domain.

The following code should give some ideas.

class Program {
    private class NonSerializableException : Exception { }

    static void Main(string[] args) {
        var childAppDomain = AppDomain.CreateDomain("Child");
        Console.WriteLine("Created child AppDomain #{0}.", childAppDomain.Id);

        // I did not create a new assembly for the helper class because I am lazy :)
        var helperAssemblyLocation = typeof(AppDomainHelper).Assembly.Location;
        var helper = (AppDomainHelper)childAppDomain.CreateInstanceFromAndUnwrap(
                helperAssemblyLocation, typeof(AppDomainHelper).FullName);
        helper.Initialize(UnloadHelper.Instance);

        childAppDomain.DoCallBack(
            () => new Thread(delegate() { throw new NonSerializableException(); }).Start());

        Console.ReadLine();
    }

    private sealed class UnloadHelper : MarshalByRefObject, IAppDomainUnloader {
        public static readonly UnloadHelper Instance = new UnloadHelper();

        private UnloadHelper() { }

        public override object InitializeLifetimeService() {
            return null;
        }

        public void RequestUnload(int id) {
            // Add application domain identified by id into unload queue.
            Console.WriteLine("AppDomain #{0} requests unload.", id);
        }
    }
}

// These two types could be in another helper assembly

public interface IAppDomainUnloader {
    void RequestUnload(int id);
}

public sealed class AppDomainHelper : MarshalByRefObject {
    public void Initialize(IAppDomainUnloader unloader) {
        AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
                unloader.RequestUnload(AppDomain.CurrentDomain.Id);
    }
}
like image 150
mgronber Avatar answered Oct 15 '22 01:10

mgronber