diff --git a/README.md b/README.md index 861d945..156c3ee 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,57 @@ |:-|---|---| | System.Net | [![Build Status](https://dev.azure.com/nanoframework/System.Net/_apis/build/status/System.Net?repoName=nanoframework%2FSystem.Net&branchName=main)](https://dev.azure.com/nanoframework/System.Net/_build/latest?definitionId=20&repoName=nanoframework%2FSystem.Net&branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.System.Net.svg?label=NuGet&style=flat&logo=nuget)](https://www.nuget.org/packages/nanoFramework.System.Net/) | +## NetworkHelper usage + +`NetworkHelper` provides two patterns for establishing a network connection: a blocking token-based approach for simple use-cases, and an event-based approach for background connection management. + +### Token-based (retryable) + +Call `SetupAndConnectNetwork` with a `CancellationToken` timeout. This method can be called repeatedly — if the first attempt times out, call it again: + +```csharp +bool connected = false; +while (!connected) +{ + CancellationTokenSource cs = new(30000); + connected = NetworkHelper.SetupAndConnectNetwork(requiresDateTime: true, token: cs.Token); + if (!connected) + { + Debug.WriteLine($"Network not ready, status: {NetworkHelper.Status}"); + // wait before retrying + Thread.Sleep(5000); + } +} +``` + +### Event-based + +Call `SetupNetworkHelper` once at startup. The helper connects in the background. Wait on `NetworkReady`: + +```csharp +NetworkHelper.SetupNetworkHelper(requiresDateTime: true); + +if (!NetworkHelper.NetworkReady.WaitOne(30000, true)) +{ + Debug.WriteLine($"Failed to connect: {NetworkHelper.Status}"); +} +``` + +> **Note:** `NetworkReady` is reset when the connection is lost and re-signaled when it is restored, accurately reflecting live network state. Code that previously assumed `NetworkReady` would remain set after first connect should be updated to handle transient disconnects. + +### Reset and reconfigure + +Call `Reset()` to fully reset the helper so it can be called again with different settings, or to restart after an error: + +```csharp +NetworkHelper.Reset(); + +// Now call SetupNetworkHelper or SetupAndConnectNetwork again +NetworkHelper.SetupNetworkHelper(requiresDateTime: true); +``` + +`SetupNetworkHelper` throws `InvalidOperationException` if called a second time without a prior `Reset()`. Token-based methods (`SetupAndConnectNetwork`) do not have this restriction and are always retryable. + ## Feedback and documentation For documentation, providing feedback, issues and finding out how to contribute please refer to the [Home repo](https://github.com/nanoframework/Home). diff --git a/Tests/NetworkHelperTests/ConnectToEthernetTests.cs b/Tests/NetworkHelperTests/ConnectToEthernetTests.cs index ecdb406..7a55754 100644 --- a/Tests/NetworkHelperTests/ConnectToEthernetTests.cs +++ b/Tests/NetworkHelperTests/ConnectToEthernetTests.cs @@ -31,8 +31,7 @@ public void TestFixedIPAddress_01() Assert.IsTrue(success); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -47,8 +46,7 @@ public void TestFixedIPAddress_02() // wait 10 seconds to connect to the network Assert.IsTrue(NetworkHelper.NetworkReady.WaitOne(10000, true)); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -64,8 +62,7 @@ public void TestDhcp_01() Assert.IsTrue(success); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -76,8 +73,7 @@ public void TestDhcp_02() // wait 10 seconds to connect to the network and get an IP address Assert.IsTrue(NetworkHelper.NetworkReady.WaitOne(10000, true)); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -88,9 +84,51 @@ public void TestSingleUsage() // call once, it's OK NetworkHelper.SetupNetworkHelper(); - // call twice, it's a NO NO and should throw an exception + // call twice without Reset — must throw NetworkHelper.SetupNetworkHelper(); }); + + NetworkHelper.Reset(); + } + + [TestMethod] + public void TestRetryAfterTimeout() + { + // First attempt: very short timeout so it expires + CancellationTokenSource cs1 = new(1000); + var firstResult = NetworkHelper.SetupAndConnectNetwork(token: cs1.Token); + + Assert.IsFalse(firstResult, "First call should have timed out"); + Assert.IsTrue(NetworkHelper.Status == NetworkHelperStatus.TokenExpiredWaitingIPAddress); + + // Second attempt: longer timeout — must not throw InvalidOperationException + CancellationTokenSource cs2 = new(10000); + var secondResult = NetworkHelper.SetupAndConnectNetwork(token: cs2.Token); + + // If there is a network, second attempt should succeed; + // if not, it will time out again — either way, it must NOT throw + Assert.IsTrue( + NetworkHelper.Status == NetworkHelperStatus.NetworkIsReady || + NetworkHelper.Status == NetworkHelperStatus.TokenExpiredWaitingIPAddress || + NetworkHelper.Status == NetworkHelperStatus.TokenExpiredWaitingDateTime, + "Expected a terminal status after the second attempt"); + + NetworkHelper.Reset(); + } + + [TestMethod] + public void TestResetAllowsSetupNetworkHelperRestart() + { + NetworkHelper.SetupNetworkHelper(); + + // Reset and call again — must not throw + NetworkHelper.Reset(); + NetworkHelper.SetupNetworkHelper(); + + // wait briefly + NetworkHelper.NetworkReady.WaitOne(5000, true); + + NetworkHelper.Reset(); } public void DisplayLastError(bool success) diff --git a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs index 13a5f29..04696d3 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs @@ -27,8 +27,11 @@ public static class NetworkHelper private static IPConfiguration _ipConfiguration; + private static Thread _workerThread; + private static bool _stopRequested; + /// - /// This flag will make sure there is only one and only call to any of the helper methods. + /// This flag will make sure there is only one and only one call to the event-based helper methods. /// private static bool _helperInstanciated = false; @@ -37,7 +40,12 @@ public static class NetworkHelper /// /// /// The conditions for this are setup in the call to . - /// It will be a composition of network connected, IpAddress available and valid system . + /// It will be a composition of network connected, IpAddress available and valid system . + /// + /// When using , this event is reset when the connection is lost + /// and re-signaled when it is restored, accurately reflecting live network state. + /// + /// public static ManualResetEvent NetworkReady => _networkReady; /// @@ -55,7 +63,7 @@ public static class NetworkHelper /// That will be the network connection to be up, having a valid IpAddress and optionally for a valid date and time to become available. /// /// Set to if valid date and time are required. - /// If any of the methods is called more than once. + /// If called more than once without an intervening call to . /// There is no network interface configured. Open the 'Edit Network Configuration' in Device Explorer and configure one. public static void SetupNetworkHelper(bool requiresDateTime = false) { @@ -64,7 +72,8 @@ public static void SetupNetworkHelper(bool requiresDateTime = false) SetupHelper(true); // fire working thread - new Thread(WorkingThread).Start(); + _workerThread = new Thread(WorkingThread); + _workerThread.Start(); } /// @@ -73,6 +82,7 @@ public static void SetupNetworkHelper(bool requiresDateTime = false) /// /// The static IP configuration you want to apply. /// Set to if valid date and time are required. + /// If called more than once without an intervening call to . /// There is no network interface configured. Open the 'Edit Network Configuration' in Device Explorer and configure one. public static void SetupNetworkHelper( IPConfiguration ipConfiguration, @@ -84,11 +94,13 @@ public static void SetupNetworkHelper( SetupHelper(true); // fire working thread - new Thread(WorkingThread).Start(); + _workerThread = new Thread(WorkingThread); + _workerThread.Start(); } /// /// This will wait for the network connection to be up and optionally for a valid date and time to become available. + /// This method is retryable and can be called multiple times after a previous call times out or fails. /// /// A used for timing out the operation. /// Set to if valid date and time are required. @@ -102,6 +114,7 @@ public static bool SetupAndConnectNetwork( /// /// This will wait for the network connection to be up and optionally for a valid date and time to become available. + /// This method is retryable and can be called multiple times after a previous call times out or fails. /// /// The static IPv4 configuration to apply to the Ethernet network interface. /// A used for timing out the operation. @@ -122,6 +135,44 @@ public static bool SetupAndConnectNetwork( requiresDateTime); } + /// + /// Resets the to its initial state, allowing to be called again + /// or the network configuration to be changed. + /// + /// + /// Call this before switching network configuration or restarting the event-based helper. + /// This method does not disconnect the network interface or alter IP settings. + /// + public static void Reset() + { + // deregister event handler to prevent a handler leak + NetworkChange.NetworkAddressChanged -= AddressChangedCallback; + + // signal the worker thread to stop and unblock it if it is waiting for an IP address + _stopRequested = true; + + if (_ipAddressAvailable != null) + { + _ipAddressAvailable.Set(); + } + + if (_workerThread != null) + { + // give the thread a moment to exit cleanly before clearing shared state + _workerThread.Join(1000); + _workerThread = null; + } + + _stopRequested = false; + _helperInstanciated = false; + _ipAddressAvailable = null; + _networkReady = new(false); + _requiresDateTime = false; + _networkHelperStatus = NetworkHelperStatus.None; + _helperException = null; + _ipConfiguration = null; + } + internal static bool InternalWaitNetworkAvailable( NetworkInterfaceType networkInterface, ref NetworkHelperStatus helperStatus, @@ -194,20 +245,32 @@ internal static bool InternalWaitNetworkAvailable( private static void WorkingThread() { // check if we have an IP - if(!NetworkHelperInternal.CheckIP( + if (!NetworkHelperInternal.CheckIP( _workingNetworkInterface, _ipConfiguration)) { - // wait here until we have an IP address + // wait here until we have an IP address or until Reset() unblocks us _ipAddressAvailable.WaitOne(); } + // bail out if Reset() was called while we were waiting + if (_stopRequested) + { + return; + } + if (_requiresDateTime) { // wait until there is a valid DateTime NetworkHelperInternal.WaitForValidDateTime(); } + // bail out if Reset() was called during the DateTime wait + if (_stopRequested) + { + return; + } + // all conditions met _networkReady.Set(); @@ -217,68 +280,73 @@ private static void WorkingThread() private static void AddressChangedCallback(object sender, EventArgs e) { - if(NetworkHelperInternal.CheckIP( + if (_stopRequested) + { + return; + } + + if (NetworkHelperInternal.CheckIP( _workingNetworkInterface, _ipConfiguration)) { _ipAddressAvailable.Set(); + + // re-signal ready; check DateTime condition in case it was required + if (!_requiresDateTime || DateTime.UtcNow.Year >= 2021) + { + _networkReady.Set(); + _networkHelperStatus = NetworkHelperStatus.NetworkIsReady; + } + } + else + { + // IP was lost - reset signals so callers block until the connection is restored + _networkReady.Reset(); + _ipAddressAvailable.Reset(); + _networkHelperStatus = NetworkHelperStatus.Reconnecting; } } /// /// Perform setup of the various fields and events, along with any of the required event handlers. /// - /// Set true to setup the events. Required for the thread approach. Not required for the CancelationToken implementation. + /// Set to setup the events and background thread. Required for the event-based approach. Not required for the CancellationToken approach. private static void SetupHelper(bool setupEvents) { - if (_helperInstanciated) - { - throw new InvalidOperationException(); - } - else + if (setupEvents) { + if (_helperInstanciated) + { + throw new InvalidOperationException(); + } + // set flag _helperInstanciated = true; // setup event _ipAddressAvailable = new(false); + } - NetworkInterface[] nis = NetworkInterface.GetAllNetworkInterfaces(); + NetworkInterface[] nis = NetworkInterface.GetAllNetworkInterfaces(); - if (setupEvents) + if (setupEvents) + { + // check if there are any network interfaces setup + if (nis.Length == 0) { - // check if there are any network interface setup - if (nis.Length == 0) - { - _networkHelperStatus = NetworkHelperStatus.FailedNoNetworkInterface; - - throw new NotSupportedException(); - } + _networkHelperStatus = NetworkHelperStatus.FailedNoNetworkInterface; - // setup handler - NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(AddressChangedCallback); + throw new NotSupportedException(); } - NetworkHelperInternal.InternalSetupHelper(nis, _workingNetworkInterface, _ipConfiguration); - - // update status - _networkHelperStatus = NetworkHelperStatus.Started; + // setup handler + NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(AddressChangedCallback); } - } - - /// - /// Method to reset internal fields to it's defaults - /// ONLY TO BE USED BY UNIT TESTS - /// - internal static void ResetInstance() - { - _ipAddressAvailable = null; - _networkReady = new(false); - _requiresDateTime = false; - _networkHelperStatus = NetworkHelperStatus.None; - _helperException = null; - _ipConfiguration = null; - _helperInstanciated = false; + + NetworkHelperInternal.InternalSetupHelper(nis, _workingNetworkInterface, _ipConfiguration); + + // update status + _networkHelperStatus = NetworkHelperStatus.Started; } } -} +} \ No newline at end of file diff --git a/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs b/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs index 6a4f077..57f7895 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs @@ -45,6 +45,11 @@ public enum NetworkHelperStatus /// /// An exception occurred with waiting for the network to become ready. Check HelperException property to find the that was thrown. /// - ExceptionOccurred + ExceptionOccurred, + + /// + /// The network was previously ready but the IP address was lost. Waiting for the connection to be restored. + /// + Reconnecting } }