File: net\System\Net\NetworkInformation\TeredoHelper.cs
Project: ndp\fx\src\System.csproj (System)
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
 
 
namespace System.Net.NetworkInformation
{
    // This class wraps the native API NotifyStableUnicastIpAddressTable.  The native function's behavior is:
    // 
    // 1. If the address table is already stable, it returns ERROR_SUCCESS and a Mib table handle that we must free.
    //    The passed-in callback will never be called, and the cancelHandle is set to NULL.
    //
    // 2. If the address table is not stable, it returns ERROR_IO_PENDING.  The table handle is set to NULL,
    //    and the cancelHandle is set to a valid handle.  The callback will be called (on a native threadpool thread)
    //    EVERY TIME the address table becomes stable until CancelMibChangeNotify2 is called on the cancelHandle
    //    (via cancelHandle.Dispose()).
    //
    // CancelMibChangeNotify2 guarantees that, by the time it returns, all calls to the callback will be complete
    // and that no new calls to the callback will be issued.
    //
    // The major concerns of the class are: 1) making sure none of the managed objects needed to handle a native
    // callback are GC'd before the callback, and 2) making sure no native callbacks will try to call into an unloaded
    // AppDomain.
 
    [SuppressUnmanagedCodeSecurity]
    internal class TeredoHelper
    {
        // Holds a list of all pending calls to NotifyStableUnicastIpAddressTable.  Also used as a lock to protect its
        // contents and impendingAppDomainUnload.
        private static List<TeredoHelper> pendingNotifications;
 
        // Flag that gets set when an AppDomain unload is imminent.  When this is set, no more calls to 
        // NotifyStableUnicastIpAddressTable are allowed.
        private static bool impendingAppDomainUnload;
 
        private readonly Action<object> callback;
        private readonly object state;
 
        private bool runCallbackCalled;
 
        // We explicitly keep a copy of this to prevent it from getting GC'd.
        private readonly StableUnicastIpAddressTableDelegate onStabilizedDelegate;
 
        // Used to cancel notification after receiving the first callback, or when the AppDomain is going down.
        private SafeCancelMibChangeNotify cancelHandle;
 
        static TeredoHelper()
        {
            pendingNotifications = new List<TeredoHelper>();
            AppDomain.CurrentDomain.DomainUnload += new EventHandler(OnAppDomainUnload);
        }
 
        private TeredoHelper(Action<object> callback, object state)
        {
            this.callback = callback;
            this.state = state;
            this.onStabilizedDelegate = new StableUnicastIpAddressTableDelegate(OnStabilized);
            this.runCallbackCalled = false;
        }
 
        // Returns true if the address table is already stable.  Otherwise, calls callback when it becomes stable.
        // 'Unsafe' because it does not flow ExecutionContext to the callback.
        public static bool UnsafeNotifyStableUnicastIpAddressTable(Action<object> callback, object state)
        {
            GlobalLog.Assert(callback != null, 
                "UnsafeNotifyStableUnicastIpAddressTable called without specifying callback!");
 
            TeredoHelper helper = new TeredoHelper(callback, state);
 
            uint err = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
            SafeFreeMibTable table = null;
 
            lock (pendingNotifications)
            {
                // If OnAppDomainUnload gets the lock first, tell our caller that we'll finish async.  Their AppDomain 
                // is about to go down anyways.  If we do, hold the lock until we've added helper to the 
                // pendingNotifications list (if we're going to complete asynchronously).
                if (impendingAppDomainUnload)
                {
                    return false;
                }
 
                err = UnsafeNetInfoNativeMethods.NotifyStableUnicastIpAddressTable(AddressFamily.Unspecified,
                    out table, helper.onStabilizedDelegate, IntPtr.Zero, out helper.cancelHandle);
 
                if (table != null)
                {
                    table.Dispose();
                }
 
                if (err == UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
                {
                    GlobalLog.Assert(!helper.cancelHandle.IsInvalid,
                        "CancelHandle invalid despite returning ERROR_IO_PENDING");
 
                    // Async completion: add us to the pendingNotifications list so we'll be canceled in the
                    // event of an AppDomain unload.
                    pendingNotifications.Add(helper);
                    return false;
                }
            }
 
            if (err != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
            {
                throw new Win32Exception((int)err);
            }
 
            return true;
        }
 
        private static void OnAppDomainUnload(object sender, EventArgs args)
        {
            // We need to ensure that we won't get any callbacks from iphlpapi after the AppDomain goes down.
            // Set impendingAppDomainUnload to ensure that no new calls to NotifyStableUnicastIpAddressTable are 
            // made, then cancel any in-progress notifications by closing their cancelHandles.
            //
            // We intentionally leave all of the canceled TeredoHelpers in the list.  impendingAppDomainUnload
            // prevents anyone from looking at the list, and it will all be GC'd as soon as the AppDomain goes
            // down anyways.
            lock (pendingNotifications)
            {
                impendingAppDomainUnload = true;
                
                foreach (TeredoHelper helper in pendingNotifications)
                {
                    GlobalLog.Assert(helper.cancelHandle != null && !helper.cancelHandle.IsInvalid,
                        "Invalid cancelHandle in pendingNotifications!");
 
                    helper.cancelHandle.Dispose();
                }
            }
        }
 
        private void RunCallback(object o)
        {
            GlobalLog.Assert(runCallbackCalled, "RunCallback called without setting runCallbackCalled!");
 
            // If OnAppDomainUnload beats us to the lock, do nothing: the AppDomain is going down soon anyways.
            // Otherwise, wait until the call to CancelMibChangeNotify2 is done before giving it up.
            lock (pendingNotifications)
            {
                if (impendingAppDomainUnload)
                {
                    return;
                }
 
#if DEBUG
                bool successfullyRemoved = pendingNotifications.Remove(this);
                GlobalLog.Assert(successfullyRemoved, 
                    "RunCallback for a TeredoHelper which is not in pendingNotifications!");
#else
                pendingNotifications.Remove(this);
#endif
 
                GlobalLog.Assert(cancelHandle != null && !cancelHandle.IsInvalid,
                    "Invalid cancelHandle in RunCallback");
 
                cancelHandle.Dispose();
            }
 
            callback.Invoke(state);
        }
 
        // This callback gets run on a native worker thread, which we don't want to allow arbitrary user code to
        // execute on (it will block AppDomain unload, for one).  Free the MibTable and delegate (exactly once)
        // to the managed ThreadPool for the rest of the processing.
        //
        // We can't use SafeHandle here for table because the marshaller doesn't support them in reverse p/invokes.
        // We won't get an AppDomain unload here anyways, because OnAppDomainUnloaded will block until all of these
        // callbacks are done.
        private void OnStabilized(IntPtr context, IntPtr table)
        {
            UnsafeNetInfoNativeMethods.FreeMibTable(table);
 
            // Lock the TeredoHelper instance to ensure that only the first call to OnStabilized will get to call 
            // RunCallback.  This is the only place that TeredoHelpers get locked, as individual instances are not
            // exposed to higher layers, so there's no chance for deadlock.
            if (!runCallbackCalled)
            {
                lock (this)
                {
                    if (!runCallbackCalled)
                    {
                        runCallbackCalled = true;
                        ThreadPool.UnsafeQueueUserWorkItem(RunCallback, null);
                    }
                }
            }
        }
    }
}