|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace System.Net.NetworkInformation
{
// Linux implementation of NetworkChange
public partial class NetworkChange
{
private static Socket? s_socket;
private static Socket? Socket
{
get
{
Debug.Assert(Monitor.IsEntered(s_gate));
return s_socket;
}
set
{
Debug.Assert(Monitor.IsEntered(s_gate));
s_socket = value;
}
}
// Lock controlling access to delegate subscriptions, socket, availability-changed state and timer.
private static readonly object s_gate = new object();
// The "leniency" window for NetworkAvailabilityChanged socket events.
// All socket events received within this duration will be coalesced into a
// single event. Generally, many route changed events are fired in succession,
// and we are not interested in all of them, just the fact that network availability
// has potentially changed as a result.
private const int AvailabilityTimerWindowMilliseconds = 150;
private static readonly TimerCallback s_availabilityTimerFiredCallback = OnAvailabilityTimerFired;
private static Timer? s_availabilityTimer;
private static bool s_availabilityHasChanged;
[UnsupportedOSPlatform("illumos")]
[UnsupportedOSPlatform("solaris")]
public static event NetworkAddressChangedEventHandler? NetworkAddressChanged
{
add
{
if (value != null)
{
lock (s_gate)
{
if (Socket == null)
{
CreateSocket();
}
s_addressChangedSubscribers.TryAdd(value, ExecutionContext.Capture());
}
}
}
remove
{
if (value != null)
{
lock (s_gate)
{
if (s_addressChangedSubscribers.Count == 0 && s_availabilityChangedSubscribers.Count == 0)
{
Debug.Assert(Socket == null,
"Socket is not null, but there are no subscribers to NetworkAddressChanged or NetworkAvailabilityChanged.");
return;
}
s_addressChangedSubscribers.Remove(value);
if (s_addressChangedSubscribers.Count == 0 && s_availabilityChangedSubscribers.Count == 0)
{
CloseSocket();
}
}
}
}
}
[UnsupportedOSPlatform("illumos")]
[UnsupportedOSPlatform("solaris")]
public static event NetworkAvailabilityChangedEventHandler? NetworkAvailabilityChanged
{
add
{
if (value != null)
{
lock (s_gate)
{
if (Socket == null)
{
CreateSocket();
}
if (s_availabilityTimer == null)
{
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever
using (ExecutionContext.SuppressFlow())
{
s_availabilityTimer = new Timer(s_availabilityTimerFiredCallback, null, Timeout.Infinite, Timeout.Infinite);
}
}
s_availabilityChangedSubscribers.TryAdd(value, ExecutionContext.Capture());
}
}
}
remove
{
if (value != null)
{
lock (s_gate)
{
if (s_addressChangedSubscribers.Count == 0 && s_availabilityChangedSubscribers.Count == 0)
{
Debug.Assert(Socket == null,
"Socket is not null, but there are no subscribers to NetworkAddressChanged or NetworkAvailabilityChanged.");
return;
}
s_availabilityChangedSubscribers.Remove(value);
if (s_availabilityChangedSubscribers.Count == 0)
{
if (s_availabilityTimer != null)
{
s_availabilityTimer.Dispose();
s_availabilityTimer = null;
s_availabilityHasChanged = false;
}
if (s_addressChangedSubscribers.Count == 0)
{
CloseSocket();
}
}
}
}
}
}
private static unsafe void CreateSocket()
{
Debug.Assert(Monitor.IsEntered(s_gate));
Debug.Assert(Socket == null, "Socket is not null, must close existing socket before opening another.");
var sh = new SafeSocketHandle();
IntPtr newSocket;
Interop.Error result = Interop.Sys.CreateNetworkChangeListenerSocket(&newSocket);
if (result != Interop.Error.SUCCESS)
{
string message = Interop.Sys.GetLastErrorInfo().GetErrorMessage();
sh.Dispose();
throw new NetworkInformationException(message);
}
Marshal.InitHandle(sh, newSocket);
Socket = new Socket(sh);
using (ExecutionContext.SuppressFlow())
{
_ = ReadEventsAsync(Socket);
}
}
private static void CloseSocket()
{
Debug.Assert(Monitor.IsEntered(s_gate));
Debug.Assert(Socket != null, "Socket was null when CloseSocket was called.");
Socket.Dispose();
Socket = null;
}
private static async Task ReadEventsAsync(Socket socket)
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
try
{
while (true)
{
// Wait for data to become available.
await socket.ReceiveAsync(Array.Empty<byte>(), SocketFlags.None).ConfigureAwait(false);
Interop.Error result = ReadEvents(socket);
if (result != Interop.Error.SUCCESS &&
result != Interop.Error.EAGAIN)
{
throw new Win32Exception(result.Info().RawErrno);
}
}
}
catch (ObjectDisposedException)
{
// Socket disposed.
}
catch (SocketException se) when (se.SocketErrorCode == SocketError.OperationAborted)
{
// ReceiveAsync aborted by disposing Socket.
}
catch (Exception ex)
{
// Unexpected error.
Debug.Fail($"Unexpected error: {ex}");
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(ex);
}
static unsafe Interop.Error ReadEvents(Socket socket)
=> Interop.Sys.ReadEvents(socket.SafeHandle, &ProcessEvent);
}
[UnmanagedCallersOnly]
private static void ProcessEvent(IntPtr socket, Interop.Sys.NetworkChangeKind kind)
{
if (kind != Interop.Sys.NetworkChangeKind.None)
{
lock (s_gate)
{
// It's safe to compare raw handle values because ProcessEvents gets
// called from ReadEvents which holds a reference on the SafeHandle.
if (Socket != null && socket == Socket.Handle)
{
OnSocketEvent(kind);
}
}
}
}
private static void OnSocketEvent(Interop.Sys.NetworkChangeKind kind)
{
switch (kind)
{
case Interop.Sys.NetworkChangeKind.AddressAdded:
case Interop.Sys.NetworkChangeKind.AddressRemoved:
OnAddressChanged();
break;
case Interop.Sys.NetworkChangeKind.AvailabilityChanged:
lock (s_gate)
{
if (s_availabilityTimer != null)
{
if (!s_availabilityHasChanged)
{
s_availabilityTimer.Change(AvailabilityTimerWindowMilliseconds, -1);
}
s_availabilityHasChanged = true;
}
}
break;
}
}
private static void OnAddressChanged()
{
Dictionary<NetworkAddressChangedEventHandler, ExecutionContext?>? addressChangedSubscribers = null;
lock (s_gate)
{
if (s_addressChangedSubscribers.Count > 0)
{
addressChangedSubscribers = new Dictionary<NetworkAddressChangedEventHandler, ExecutionContext?>(s_addressChangedSubscribers);
}
}
if (addressChangedSubscribers != null)
{
foreach (KeyValuePair<NetworkAddressChangedEventHandler, ExecutionContext?>
subscriber in addressChangedSubscribers)
{
NetworkAddressChangedEventHandler handler = subscriber.Key;
ExecutionContext? ec = subscriber.Value;
if (ec == null) // Flow suppressed
{
handler(null, EventArgs.Empty);
}
else
{
ExecutionContext.Run(ec, s_runAddressChangedHandler, handler);
}
}
}
}
private static void OnAvailabilityTimerFired(object? state)
{
Dictionary<NetworkAvailabilityChangedEventHandler, ExecutionContext?>? availabilityChangedSubscribers = null;
lock (s_gate)
{
if (s_availabilityHasChanged)
{
s_availabilityHasChanged = false;
if (s_availabilityChangedSubscribers.Count > 0)
{
availabilityChangedSubscribers =
new Dictionary<NetworkAvailabilityChangedEventHandler, ExecutionContext?>(
s_availabilityChangedSubscribers);
}
}
}
if (availabilityChangedSubscribers != null)
{
bool isAvailable = NetworkInterface.GetIsNetworkAvailable();
NetworkAvailabilityEventArgs args = isAvailable ? s_availableEventArgs : s_notAvailableEventArgs;
ContextCallback callbackContext = isAvailable ? s_runHandlerAvailable : s_runHandlerNotAvailable;
foreach (KeyValuePair<NetworkAvailabilityChangedEventHandler, ExecutionContext?>
subscriber in availabilityChangedSubscribers)
{
NetworkAvailabilityChangedEventHandler handler = subscriber.Key;
ExecutionContext? ec = subscriber.Value;
if (ec == null) // Flow suppressed
{
handler(null, args);
}
else
{
ExecutionContext.Run(ec, callbackContext, handler);
}
}
}
}
}
}
|