Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Microsoft.Maui.Networking
partial class ConnectivityImplementation : IConnectivity
{
#if !(MACCATALYST || MACOS)
// TODO: Use NWPathMonitor on > iOS 12
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
static readonly Lazy<CTCellularData> cellularData = new Lazy<CTCellularData>(() => new CTCellularData());

Expand Down Expand Up @@ -41,7 +40,6 @@ public NetworkAccess NetworkAccess
{
var restricted = false;
#if !(MACCATALYST || MACOS)
// TODO: Use NWPathMonitor on > iOS 12
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
restricted = CellularData.RestrictedState == CTCellularDataRestrictedState.Restricted;
#pragma warning restore BI1234, CA1416
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
#if !(MACCATALYST || MACOS)
using CoreTelephony;
#endif
using CoreFoundation;
using SystemConfiguration;
using Network;

namespace Microsoft.Maui.Networking
{
Expand All @@ -19,144 +18,122 @@ enum NetworkStatus

static class Reachability
{
internal const string HostName = "www.microsoft.com";
static NWPathMonitor sharedMonitor;
static readonly object monitorLock = new object();

internal static NetworkStatus RemoteHostStatus()
static NWPathMonitor SharedMonitor
{
using (var remoteHostReachability = new NetworkReachability(HostName))
get
{
var reachable = remoteHostReachability.TryGetFlags(out var flags);
lock (monitorLock)
{
if (sharedMonitor == null)
{
sharedMonitor = new NWPathMonitor();
sharedMonitor.SetQueue(DispatchQueue.DefaultGlobalQueue);
sharedMonitor.Start();
}
return sharedMonitor;
}
}
}

if (!reachable)
return NetworkStatus.NotReachable;
static NWPath GetCurrentPath()
{
var monitor = SharedMonitor;
var path = monitor?.CurrentPath;

// If path is null, the monitor might not have received its first update yet
// Force initialization by accessing the monitor
if (path == null && monitor != null)
{
// Monitor is started but path not yet available - this is expected on first call
// The monitor will update CurrentPath asynchronously
}

return path;
}

if (!IsReachableWithoutRequiringConnection(flags))
return NetworkStatus.NotReachable;
internal static NetworkStatus RemoteHostStatus()
{
var path = GetCurrentPath();
if (path == null || path.Status != NWPathStatus.Satisfied)
return NetworkStatus.NotReachable;

#if __IOS__
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
return NetworkStatus.ReachableViaCarrierDataNetwork;
if (path.UsesInterfaceType(NWInterfaceType.Cellular))
return NetworkStatus.ReachableViaCarrierDataNetwork;
#endif

return NetworkStatus.ReachableViaWiFiNetwork;
}
return NetworkStatus.ReachableViaWiFiNetwork;
}

internal static NetworkStatus InternetConnectionStatus()
{
var status = NetworkStatus.NotReachable;

var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
var path = GetCurrentPath();
if (path == null || path.Status != NWPathStatus.Satisfied)
return NetworkStatus.NotReachable;

#if __IOS__
// If it's a WWAN connection..
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
status = NetworkStatus.ReachableViaCarrierDataNetwork;
if (path.UsesInterfaceType(NWInterfaceType.Cellular))
return NetworkStatus.ReachableViaCarrierDataNetwork;
#endif

// If the connection is reachable and no connection is required, then assume it's WiFi
if (defaultNetworkAvailable)
{
status = NetworkStatus.ReachableViaWiFiNetwork;
}

// If the connection is on-demand or on-traffic and no user intervention
// is required, then assume WiFi.
if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) &&
(flags & NetworkReachabilityFlags.InterventionRequired) == 0)
{
status = NetworkStatus.ReachableViaWiFiNetwork;
}

return status;
return NetworkStatus.ReachableViaWiFiNetwork;
}

internal static IEnumerable<NetworkStatus> GetActiveConnectionType()
{
var status = new List<NetworkStatus>();
var path = GetCurrentPath();

var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
if (path == null || path.Status != NWPathStatus.Satisfied)
return status;

#if __IOS__
// If it's a WWAN connection.
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
if (path.UsesInterfaceType(NWInterfaceType.Cellular))
{
status.Add(NetworkStatus.ReachableViaCarrierDataNetwork);
}
else if (defaultNetworkAvailable)
else if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired))
#else
// If the connection is reachable and no connection is required, then assume it's WiFi
if (defaultNetworkAvailable)
if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired))
#endif
{
status.Add(NetworkStatus.ReachableViaWiFiNetwork);
}
else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) &&
(flags & NetworkReachabilityFlags.InterventionRequired) == 0)
{
// If the connection is on-demand or on-traffic and no user intervention
// is required, then assume WiFi.
status.Add(NetworkStatus.ReachableViaWiFiNetwork);
}

return status;
}

internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags)
internal static bool IsNetworkAvailable()
{
var ip = new IPAddress(0);
using (var defaultRouteReachability = new NetworkReachability(ip))
{
if (!defaultRouteReachability.TryGetFlags(out flags))
return false;

return IsReachableWithoutRequiringConnection(flags);
}
}

internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags)
{
// Is it reachable with the current network configuration?
var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0;

// Do we need a connection to reach it?
var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0;

#if __IOS__
// Since the network stack will automatically try to get the WAN up,
// probe that
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
noConnectionRequired = true;
#endif

return isReachable && noConnectionRequired;
var path = GetCurrentPath();
return path != null && path.Status == NWPathStatus.Satisfied;
}
}

class ReachabilityListener : IDisposable
{
NetworkReachability defaultRouteReachability;
NetworkReachability remoteHostReachability;
// Delay to allow connection status to stabilize before notifying listeners
const int ConnectionStatusChangeDelayMs = 100;

NWPathMonitor pathMonitor;
Action<NWPath> pathUpdateHandler;

internal ReachabilityListener()
{
var ip = new IPAddress(0);
defaultRouteReachability = new NetworkReachability(ip);
#pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13
defaultRouteReachability.SetNotification(OnChange);
defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault);
#pragma warning restore CA1422

remoteHostReachability = new NetworkReachability(Reachability.HostName);

// Need to probe before we queue, or we wont get any meaningful values
// this only happens when you create NetworkReachability from a hostname
remoteHostReachability.TryGetFlags(out var flags);

#pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13
remoteHostReachability.SetNotification(OnChange);
remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault);
#pragma warning restore CA1422
pathMonitor = new NWPathMonitor();
pathUpdateHandler = async (NWPath path) =>
{
// Add in artificial delay so the connection status has time to change
await Task.Delay(ConnectionStatusChangeDelayMs);
ReachabilityChanged?.Invoke();
Comment on lines +138 to +140
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using async void lambda (assigned to Action) creates fire-and-forget behavior where exceptions thrown after the await will be unhandled and could crash the application. Consider wrapping the async work in a non-async lambda that handles the task explicitly, or add try-catch around the entire body to ensure exceptions don't escape.

Suggested change
// Add in artificial delay so the connection status has time to change
await Task.Delay(ConnectionStatusChangeDelayMs);
ReachabilityChanged?.Invoke();
try
{
// Add in artificial delay so the connection status has time to change
await Task.Delay(ConnectionStatusChangeDelayMs);
ReachabilityChanged?.Invoke();
}
catch (Exception ex)
{
// Optionally log the exception, or ignore
System.Diagnostics.Debug.WriteLine($"Exception in ReachabilityListener pathUpdateHandler: {ex}");
}

Copilot uses AI. Check for mistakes.
};
Comment on lines +136 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a Action it would be ideal to have a method instead. It can be async void if needed here as there is an awaited Task.Delay


pathMonitor.SnapshotHandler = pathUpdateHandler;
pathMonitor.SetQueue(DispatchQueue.DefaultGlobalQueue);
pathMonitor.Start();

#if !(MACCATALYST || MACOS)
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
Expand All @@ -171,10 +148,14 @@ internal ReachabilityListener()

internal void Dispose()
{
defaultRouteReachability?.Dispose();
defaultRouteReachability = null;
remoteHostReachability?.Dispose();
remoteHostReachability = null;
if (pathMonitor != null)
{
pathMonitor.SnapshotHandler = null;
pathUpdateHandler = null;
pathMonitor.Cancel();
pathMonitor.Dispose();
pathMonitor = null;
}

#if !(MACCATALYST || MACOS)
#pragma warning disable CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
Expand All @@ -191,14 +172,5 @@ void OnRestrictedStateChanged(CTCellularDataRestrictedState state)
}
#pragma warning restore BI1234
#endif

async void OnChange(NetworkReachabilityFlags flags)
{
// Add in artifical delay so the connection status has time to change
// else it will return true no matter what.
await Task.Delay(100);

ReachabilityChanged?.Invoke();
}
}
}
Loading