using CoreFoundation; using Network; using VoiceCat.Core; namespace VoiceCat.iOS; // Watches which interface actually carries traffic so a handover can be acted on the instant it // happens instead of after the control keepalive proves the old path dead. // // The signal must be narrow. Roaming between access points, a lift ride, a minute of bad // cellular: those keep the same interface and the same source address, so TCP survives them and // a reconnect would be pure damage. Only the set of satisfied interfaces changing — Wi-Fi to // cellular, one physical link to another — strands the existing sockets on a source address that // no longer exists, and that is the only thing reported here. internal sealed class IosNetworkPathMonitor : IDisposable { internal static IosNetworkPathMonitor Shared { get; } = new(); private readonly DispatchQueue queue = new("voicecat.path"); private readonly object gate = new(); private readonly ControlPathWatcher watcher = new(); private NWPathMonitor? monitor; private bool started; // Raised on the monitor queue when the carrying interface changed and the new path is usable. internal event Action? InterfaceChanged; internal void Start() { lock (gate) { if (started) return; started = true; monitor = new NWPathMonitor(); monitor.SetQueue(queue); monitor.SnapshotHandler = OnPath; monitor.Start(); } } private void OnPath(NWPath path) { List interfaces = []; if (path.Status == NWPathStatus.Satisfied) path.EnumerateInterfaces(item => { interfaces.Add($"{item.InterfaceType}:{item.Name}"); return true; }); if (watcher.Observe(path.Status == NWPathStatus.Satisfied, interfaces)) InterfaceChanged?.Invoke(string.Join(",", interfaces)); } public void Dispose() { lock (gate) { monitor?.Cancel(); monitor?.Dispose(); monitor = null; started = false; watcher.Reset(); } } }