Search code examples
c#debugging.net-remoting

MarshalByRefObject becoming "disconnected at server" even while sponsored


I am writing a game which supports mods, for safety I am sandboxing the mods into a separate AppDomain from the game engine (so I can restrict the capabilities of mods separately from the engine). However, objects in the scripting domain which the engine keeps a reference to keep getting garbage collected too early and I get an exception like this:

Object '/30b08873_4929_48a5_989c_e8e5cebc601f/lhejbssq8d8qsgvuulhbkqbo_615.rem' has been disconnected or does not exist at the server.

On the server side I am creating objects like this (using AppDomainToolkit:

// Take in a "script reference" which is basically just the name of a type in the scripting domain
public Reference<T> Dereference<T>(ScriptReference reference) where T : MarshalByRefObject
{
    // Create a remote instance of this type
    T instance = (T)RemoteFunc.Invoke<ScriptReference, object>(_pluginContext.Domain, reference, r =>
    {
        var t = Type.GetType(r.TypeName, true, false);
        return Activator.CreateInstance(t);
    });

    //Return a reference which keeps this object alive until disposed
    return new Reference<T>(instance, reference.TypeName, reference.Name);
}

The idea is that Reference<T> is the sponsor for the object and as long as it stays alive the object in the remote domain will also stay alive. This is where the problem lies, I am accessing objects through their reference and yet I still get the "disconnected at server" exception. Here is my implementation of Reference<T>:

public sealed class Reference<T>
    : IDisposable
    where T : MarshalByRefObject
{
    private InnerSponsor _sponsor;

    public T RemoteObject
    {
        get { return _sponsor.RemoteObject; }
    }

    public string TypeName { get; private set; }
    public string Name { get; private set; }

    public Reference(T obj, string typeName = null, string name = null)
    {
        TypeName = typeName;
        Name = name;

        _sponsor = new InnerSponsor(obj);
    }

    ~Reference()
    {
        if (_sponsor != null)
            _sponsor.Dispose();
        _sponsor = null;
    }

    public void Dispose()
    {
        _sponsor.Dispose();
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Inner sponsor is the actual sponsor (and is kept alive by the remoting system).
    /// If all references to Reference are lost, it will dispose the sponsor in its destructor
    /// </summary>
    private sealed class InnerSponsor
        : MarshalByRefObject, ISponsor, IDisposable
    {
        private readonly ILease _lease;
        private bool _isDisposed;
        public readonly T RemoteObject;

        private bool _isUnregistered = false;

        public InnerSponsor(T obj)
        {
            RemoteObject = obj;
            _lease = (ILease)obj.GetLifetimeService();
            _lease.Register(this, SponsorTime());
        }

        public TimeSpan Renewal(ILease lease)
        {
            if (lease == null)
                throw new ArgumentNullException("lease");

            if (_isDisposed)
            {
                if (!_isUnregistered)
                {
                    _lease.Unregister(this);
                    _isUnregistered = true;
                }
                return TimeSpan.Zero;
            }

            return SponsorTime();
        }

        private static TimeSpan SponsorTime()
        {
            return TimeSpan.FromMilliseconds(/*Some value fetched from configuration*/);
        }

        public void Dispose()
        {
            _isDisposed = true;
        }
    }
}

What am I doing wrong to cause my objects to die even whilst there is a live Reference<T> to them?


Solution

  • Use the ClientSponsor class that is built into .NET. It is very stable and simplistic.

    It's what most people turn to when working across AppDomains. Your implementation looks fine, but could be as simple as your SponsorTime() not returning the correct value from the config.