From 055d4fe792f3a3c0eac63dcdec4032d1a2667679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Fri, 29 May 2026 11:00:07 +0100 Subject: [PATCH 1/2] Fix NetworkHelper re-entry guard and add `Reset()` - Scope re-entry guard to event-based setup path only, so token-based callers can retry after timeout/failure. - Add public `Reset()` to clear all state and allow reconfiguration from scratch. - Improve `AddressChangedCallback` to track IP loss and re-acquisition, resetting `NetworkReady` accordingly. - Add `Reconnecting` status enum value. - New tests covering retry-after-timeout and reset-then-restart scenarios. - Migrate all test assertions to TestFramework v3 API. - Update README with `NetworkHelper` usage guidance. --- README.md | 53 +++++++- Tests/IPAddressTests/IPAddressTests.cs | 102 ++++++++-------- .../ConnectToEthernetTests.cs | 62 +++++++--- Tests/SocketTests/SocketExceptionsTests.cs | 26 ++-- Tests/SocketTests/SocketOptionsTests.cs | 6 +- .../NetworkHelper/NetworkHelper.cs | 114 +++++++++++------- .../NetworkHelper/NetworkHelperStatus.cs | 5 + 7 files changed, 244 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 861d945..a547f4a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,58 @@ |:-|---|---| | 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/) | -## Feedback and documentation +## 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. + +## 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/IPAddressTests/IPAddressTests.cs b/Tests/IPAddressTests/IPAddressTests.cs index 49f6b45..3d50069 100644 --- a/Tests/IPAddressTests/IPAddressTests.cs +++ b/Tests/IPAddressTests/IPAddressTests.cs @@ -27,11 +27,11 @@ public void NetTest1_DNS() { IPHostEntry ipHostEntry = Dns.GetHostEntry("192.168.1.1"); - Assert.Equal(ipHostEntry.AddressList.Length, 1, "GetHostEntry returned wrong number of addresses"); + Assert.AreEqual(1, ipHostEntry.AddressList.Length, "GetHostEntry returned wrong number of addresses"); IPAddress address = ipHostEntry.AddressList[0]; - Assert.NotNull(address, "Address is null"); - Assert.Equal(address.ToString(), "192.168.1.1", "Address is incorrect"); + Assert.IsNotNull(address, "Address is null"); + Assert.AreEqual("192.168.1.1", address.ToString(), "Address is incorrect"); } [TestMethod] @@ -54,25 +54,25 @@ public void NetTest2_IPAddressBasic() + IPInts[1] * 256 + IPInts[2] * 256 * 256 + IPInts[3] * 256 * 256 * 256); - Assert.NotNull(address, "Address is null"); + Assert.IsNotNull(address, "Address is null"); Type typeOfAddress = address.GetType(); - Assert.IsType(typeOfAddress, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); + Assert.IsInstanceOfType(address, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); byte[] targetBytes = { (byte)IPInts[0], (byte)IPInts[1], (byte)IPInts[2], (byte)IPInts[3] }; byte[] addressBytes = address.GetAddressBytes(); - Assert.Equal(addressBytes.Length, 4, "GetAddressBytes returns wrong size"); + Assert.AreEqual(4, addressBytes.Length, "GetAddressBytes returns wrong size"); for (int j = 0; j < 4; j++) { - Assert.Equal(addressBytes[j], targetBytes[j], "GetAddressBytes returns wrong bytes"); + Assert.AreEqual(targetBytes[j], addressBytes[j], "GetAddressBytes returns wrong bytes"); } IPAddress addressFromByteArray = new(targetBytes); addressBytes = addressFromByteArray.GetAddressBytes(); for (int j = 0; j < 4; j++) { - Assert.Equal(addressBytes[j], targetBytes[j], "Address from byte array returns wrong bytes"); + Assert.AreEqual(targetBytes[j], addressBytes[j], "Address from byte array returns wrong bytes"); } IPAddress address2 = new( @@ -81,9 +81,9 @@ public void NetTest2_IPAddressBasic() + IPInts[2] * 256 * 256 + IPInts[3] * 256 * 256 * 256); - Assert.Equal(address.ToString(), address2.ToString(), "ToString returns differently for same data"); + Assert.AreEqual(address.ToString(), address2.ToString(), "ToString returns differently for same data"); - Assert.Equal(address.GetHashCode(), address2.GetHashCode(), "GetHasCode returns differently for same data"); + Assert.AreEqual(address.GetHashCode(), address2.GetHashCode(), "GetHasCode returns differently for same data"); address2 = new IPAddress( (IPInts[0] % 2 + 1) @@ -91,7 +91,7 @@ public void NetTest2_IPAddressBasic() + (IPInts[2] % 2 + 1) * 256 * 256 + (IPInts[3] % 2 + 1) * 256 * 256 * 256); - Assert.NotEqual(address.GetHashCode(), address2.GetHashCode(), "GetHasCode returns same for " + address.ToString() + Assert.AreNotEqual(address.GetHashCode(), address2.GetHashCode(), "GetHasCode returns same for " + address.ToString() + " as " + address2.ToString()); } } @@ -100,20 +100,20 @@ public void NetTest2_IPAddressBasic() public void NetTest3_IPAddressLoopback() { IPAddress address = IPAddress.Loopback; - Assert.NotNull(address, "Address is null"); + Assert.IsNotNull(address, "Address is null"); - Assert.Equal(address.ToString(), "127.0.0.1", "Address is incorrect"); + Assert.AreEqual("127.0.0.1", address.ToString(), "Address is incorrect"); Type typeOfAddress = address.GetType(); - Assert.IsType(typeOfAddress, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); + Assert.IsInstanceOfType(address, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); byte[] localhostBytes = { 127, 0, 0, 1 }; byte[] addressBytes = address.GetAddressBytes(); - Assert.Equal(addressBytes.Length, 4, "GetAddressBytes returns wrong size"); + Assert.AreEqual(4, addressBytes.Length, "GetAddressBytes returns wrong size"); for (int i = 0; i < 4; i++) { - Assert.Equal(addressBytes[i], localhostBytes[i], "GetAddressBytes returns wrong bytes"); + Assert.AreEqual(localhostBytes[i], addressBytes[i], "GetAddressBytes returns wrong bytes"); } } @@ -123,20 +123,20 @@ public void NetTest4_IPAddressAny() { IPAddress address = IPAddress.Any; - Assert.NotNull(address, "Address is null"); + Assert.IsNotNull(address, "Address is null"); - Assert.Equal(address.ToString(), "0.0.0.0", "Address is incorrect"); + Assert.AreEqual("0.0.0.0", address.ToString(), "Address is incorrect"); Type typeOfAddress = address.GetType(); - Assert.IsType(typeOfAddress, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); + Assert.IsInstanceOfType(address, Type.GetType("System.Net.IPAddress"), "Type is incorrect"); byte[] localhostBytes = { 0, 0, 0, 0 }; byte[] addressBytes = address.GetAddressBytes(); - Assert.Equal(addressBytes.Length, 4, "GetAddressBytes returns wrong size"); + Assert.AreEqual(4, addressBytes.Length, "GetAddressBytes returns wrong size"); for (int i = 0; i < 4; i++) { - Assert.Equal(addressBytes[i], localhostBytes[i], "GetAddressBytes returns wrong bytes"); + Assert.AreEqual(localhostBytes[i], addressBytes[i], "GetAddressBytes returns wrong bytes"); } } @@ -171,29 +171,29 @@ public void NetTest5_IPEndPointBasic() Debug.WriteLine("EndPoint2 created with long and int"); IPEndPoint endPoint2 = new(addressLong, portInt); - Assert.NotNull(endPoint1, "EndPoint1 is null"); - Assert.NotNull(endPoint2, "EndPoint2 is null"); + Assert.IsNotNull(endPoint1, "EndPoint1 is null"); + Assert.IsNotNull(endPoint2, "EndPoint2 is null"); Type typeOfEndPoint = endPoint1.GetType(); - Assert.IsType(typeOfEndPoint, Type.GetType("System.Net.IPEndPoint"), "EndPoint1 Type is incorrect"); + Assert.IsInstanceOfType(endPoint1, Type.GetType("System.Net.IPEndPoint"), "EndPoint1 Type is incorrect"); typeOfEndPoint = endPoint2.GetType(); - Assert.IsType(typeOfEndPoint, Type.GetType("System.Net.IPEndPoint"), "EndPoint2 Type is incorrect"); + Assert.IsInstanceOfType(endPoint2, Type.GetType("System.Net.IPEndPoint"), "EndPoint2 Type is incorrect"); - Assert.Equal(endPoint1.ToString(), endPoint2.ToString(), "ToString returns differently for same data"); + Assert.AreEqual(endPoint1.ToString(), endPoint2.ToString(), "ToString returns differently for same data"); - Assert.True(endPoint1.Equals(endPoint2), "Equals returns false for same data"); + Assert.IsTrue(endPoint1.Equals(endPoint2), "Equals returns false for same data"); int hashCode1 = endPoint1.GetHashCode(); int hashCode2 = endPoint2.GetHashCode(); - Assert.Equal(hashCode1, hashCode2, "GetHasCode returns differently for same data"); + Assert.AreEqual(hashCode1, hashCode2, "GetHasCode returns differently for same data"); - Assert.False(endPoint1.Address.ToString() != endPoint2.Address.ToString() + Assert.IsFalse(endPoint1.Address.ToString() != endPoint2.Address.ToString() || endPoint1.Address.ToString() != address.ToString() || endPoint2.Address.ToString() != address.ToString(), "Address returns wrong data"); - Assert.False(endPoint1.Port != endPoint2.Port + Assert.IsFalse(endPoint1.Port != endPoint2.Port || endPoint1.Port != portInt || endPoint2.Port != portInt, "Port returns wrong data"); @@ -201,17 +201,17 @@ public void NetTest5_IPEndPointBasic() endPoint2 = (IPEndPoint)endPoint2.Create(endPoint1.Serialize()); typeOfEndPoint = endPoint2.GetType(); - Assert.IsType(typeOfEndPoint, Type.GetType("System.Net.IPEndPoint"), "EndPoint2 Type is incorrect after clone"); + Assert.IsInstanceOfType(endPoint2, Type.GetType("System.Net.IPEndPoint"), "EndPoint2 Type is incorrect after clone"); - Assert.Equal(endPoint1.ToString(), endPoint2.ToString(), "ToString returns differently for cloned data"); + Assert.AreEqual(endPoint1.ToString(), endPoint2.ToString(), "ToString returns differently for cloned data"); - Assert.Equal(endPoint1.GetHashCode(), endPoint2.GetHashCode(), "GetHashCode returns differently for cloned data"); + Assert.AreEqual(endPoint1.GetHashCode(), endPoint2.GetHashCode(), "GetHashCode returns differently for cloned data"); - Assert.False(endPoint1.Address.ToString() != endPoint2.Address.ToString() + Assert.IsFalse(endPoint1.Address.ToString() != endPoint2.Address.ToString() || endPoint1.Address.ToString() != address.ToString() || endPoint2.Address.ToString() != address.ToString(), "Address returns wrong data after clone"); - Assert.False(endPoint1.Port != endPoint2.Port + Assert.IsFalse(endPoint1.Port != endPoint2.Port || endPoint1.Port != portInt || endPoint2.Port != portInt, "Port returns wrong data after clone"); @@ -225,14 +225,14 @@ public void NetTest5_IPEndPointBasic() + (IPInts[3] % 2 + 1) * 256 * 256 * 256; endPoint2 = new IPEndPoint(addressLong2, portInt2); - Assert.NotEqual(endPoint1.GetHashCode(), endPoint2.GetHashCode(), "GetHashCode returns same for " + Assert.AreNotEqual(endPoint1.GetHashCode(), endPoint2.GetHashCode(), "GetHashCode returns same for " + endPoint1.ToString() + " as " + endPoint2.ToString()); - Assert.False(endPoint1.Address == endPoint2.Address + Assert.IsFalse(endPoint1.Address == endPoint2.Address || endPoint2.Address == address, "Address returns wrong data after change"); - Assert.False(endPoint1.Port == endPoint2.Port + Assert.IsFalse(endPoint1.Port == endPoint2.Port || endPoint2.Port == portInt, "Port returns wrong data after change"); } } @@ -241,13 +241,13 @@ public void NetTest5_IPEndPointBasic() public void NetTest5_IPHostEntryBasic() { IPHostEntry ipHostEntry = Dns.GetHostEntry("192.168.1.1"); - Assert.NotNull(ipHostEntry, "IPHostEntry is null"); + Assert.IsNotNull(ipHostEntry, "IPHostEntry is null"); Type typeOfIPHostEntry = ipHostEntry.GetType(); - Assert.IsType(typeOfIPHostEntry, Type.GetType("System.Net.IPHostEntry"), "IPHostEntry Type is incorrect"); + Assert.IsInstanceOfType(ipHostEntry, Type.GetType("System.Net.IPHostEntry"), "IPHostEntry Type is incorrect"); - Assert.Equal(ipHostEntry.AddressList[0].ToString(), "192.168.1.1", "AddressList[0] is incorrect"); - Assert.Throws(typeof(IndexOutOfRangeException), () => { ipHostEntry.AddressList[1].ToString(); }); + Assert.AreEqual("192.168.1.1", ipHostEntry.AddressList[0].ToString(), "AddressList[0] is incorrect"); + Assert.ThrowsException(typeof(IndexOutOfRangeException), () => { ipHostEntry.AddressList[1].ToString(); }); } [TestMethod] @@ -278,25 +278,25 @@ public void NetTest6_SocketAddressBasic() SocketAddress socketAddress1 = ipEndpoint1.Serialize(); SocketAddress socketAddress2 = ipEndpoint1.Serialize(); - Assert.NotNull(socketAddress1, "socketAddress1 is null"); - Assert.NotNull(socketAddress2, "socketAddress2 is null"); + Assert.IsNotNull(socketAddress1, "socketAddress1 is null"); + Assert.IsNotNull(socketAddress2, "socketAddress2 is null"); Type typeOfSocketAddress = socketAddress1.GetType(); - Assert.IsType(typeOfSocketAddress, Type.GetType("System.Net.SocketAddress"), "socketAddress1 Type is incorrect"); + Assert.IsInstanceOfType(socketAddress1, Type.GetType("System.Net.SocketAddress"), "socketAddress1 Type is incorrect"); typeOfSocketAddress = socketAddress2.GetType(); - Assert.IsType(typeOfSocketAddress, Type.GetType("System.Net.SocketAddress"), "socketAddress2 Type is incorrect"); + Assert.IsInstanceOfType(socketAddress2, Type.GetType("System.Net.SocketAddress"), "socketAddress2 Type is incorrect"); - Assert.Equal(socketAddress1.ToString(), socketAddress2.ToString(), "ToString returns differently for same data"); + Assert.AreEqual(socketAddress1.ToString(), socketAddress2.ToString(), "ToString returns differently for same data"); - Assert.Equal(socketAddress1.GetHashCode(), socketAddress2.GetHashCode(), $"GetHashCode returns differently for same data"); - Assert.True(socketAddress1.Family == AddressFamily.InterNetwork, "socketAddress1 Family is incorrect"); - Assert.True(socketAddress2.Family == AddressFamily.InterNetwork, "socketAddress2 Family is incorrect"); + Assert.AreEqual(socketAddress1.GetHashCode(), socketAddress2.GetHashCode(), $"GetHashCode returns differently for same data"); + Assert.IsTrue(socketAddress1.Family == AddressFamily.InterNetwork, "socketAddress1 Family is incorrect"); + Assert.IsTrue(socketAddress2.Family == AddressFamily.InterNetwork, "socketAddress2 Family is incorrect"); // Recreate a different Socket socketAddress2 = new SocketAddress(AddressFamily.Chaos, 8); - Assert.NotEqual(socketAddress1.GetHashCode(), socketAddress2.GetHashCode(), "GetHashCode returns same for " + Assert.AreNotEqual(socketAddress1.GetHashCode(), socketAddress2.GetHashCode(), "GetHashCode returns same for " + socketAddress1.ToString() + " " + socketAddress1.GetHashCode() + " as " + socketAddress2.ToString() + " " + socketAddress2.GetHashCode()); } diff --git a/Tests/NetworkHelperTests/ConnectToEthernetTests.cs b/Tests/NetworkHelperTests/ConnectToEthernetTests.cs index e33fa78..c186cc6 100644 --- a/Tests/NetworkHelperTests/ConnectToEthernetTests.cs +++ b/Tests/NetworkHelperTests/ConnectToEthernetTests.cs @@ -29,10 +29,9 @@ public void TestFixedIPAddress_01() DisplayLastError(success); - Assert.True(success); + Assert.IsTrue(success); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -45,10 +44,9 @@ public void TestFixedIPAddress_02() new[] { "192.168.1.1" }), true); // wait 10 seconds to connect to the network - Assert.True(NetworkHelper.NetworkReady.WaitOne(10000, true)); + Assert.IsTrue(NetworkHelper.NetworkReady.WaitOne(10000, true)); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -62,10 +60,9 @@ public void TestDhcp_01() DisplayLastError(success); - Assert.True(success); + Assert.IsTrue(success); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] @@ -74,23 +71,60 @@ public void TestDhcp_02() NetworkHelper.SetupNetworkHelper(true); // wait 10 seconds to connect to the network and get an IP address - Assert.True(NetworkHelper.NetworkReady.WaitOne(10000, true)); + Assert.IsTrue(NetworkHelper.NetworkReady.WaitOne(10000, true)); - // need to reset this internal flag to allow calling the NetworkHelper again - NetworkHelper.ResetInstance(); + NetworkHelper.Reset(); } [TestMethod] public void TestSingleUsage() { - Assert.Throws(typeof(InvalidOperationException), () => + Assert.ThrowsException(typeof(InvalidOperationException), () => { // 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.None); + + 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/Tests/SocketTests/SocketExceptionsTests.cs b/Tests/SocketTests/SocketExceptionsTests.cs index d218802..76de8c1 100644 --- a/Tests/SocketTests/SocketExceptionsTests.cs +++ b/Tests/SocketTests/SocketExceptionsTests.cs @@ -25,7 +25,7 @@ public void SocketExceptionTest2_AddressAlreadyInUse() { Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { socketClient.Bind(new IPEndPoint(IPAddress.Loopback, 10)); @@ -46,7 +46,7 @@ public void SocketExceptionTest3_Protocol_Address_FamilyNotSupported() } catch (SocketException e) { - Assert.False(e.ErrorCode != (int)SocketError.ProtocolFamilyNotSupported && e.ErrorCode != (int)SocketError.AddressFamilyNotSupported, "Incorrect ErrorCode in SocketException " + Assert.IsFalse(e.ErrorCode != (int)SocketError.ProtocolFamilyNotSupported && e.ErrorCode != (int)SocketError.AddressFamilyNotSupported, "Incorrect ErrorCode in SocketException " + e.ErrorCode); return; } @@ -65,7 +65,7 @@ public void SocketExceptionTest4_ProtocolNotSupported() } catch (SocketException e) { - Assert.Equal(e.ErrorCode, (int)SocketError.ProtocolNotSupported, "Incorrect ErrorCode in SocketException " + Assert.AreEqual((int)SocketError.ProtocolNotSupported, e.ErrorCode, "Incorrect ErrorCode in SocketException " + e.ErrorCode); return; } @@ -77,7 +77,7 @@ public void SocketExceptionTest6_IsConnected() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { testSockets.Startup(0, 0); testSockets.socketServer.Listen(1); @@ -93,7 +93,7 @@ public void SocketExceptionTest6_IsConnected() public void SocketExceptionTest11_AccessDenied() { SocketPair testSockets = new SocketPair(ProtocolType.Udp, SocketType.Dgram); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { int clientPort = SocketTools.nextPort; int serverPort = SocketTools.nextPort; @@ -113,7 +113,7 @@ public void SocketExceptionTest12_NotConnected() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { testSockets.Startup(0, 0); Socket socketTemp = new Socket(AddressFamily.InterNetwork, @@ -130,7 +130,7 @@ public void SocketExceptionTest13_InvalidArgument() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { int clientPort = SocketTools.nextPort; int serverPort = SocketTools.nextPort; @@ -147,7 +147,7 @@ public void SocketExceptionTest13_InvalidArgument() public void SocketExceptionTest14_AddressNotAvailable() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { int clientPort = SocketTools.nextPort; int serverPort = SocketTools.nextPort; @@ -163,7 +163,7 @@ public void SocketExceptionTest14_AddressNotAvailable() [TestMethod] public void SocketExceptionTest16_HostNotFound() { - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { IPHostEntry ipHostEntry = Dns.GetHostEntry("fakeHostName"); }); @@ -172,7 +172,7 @@ public void SocketExceptionTest16_HostNotFound() [TestMethod] public void SocketExceptionTest17_SocketError() { - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { SocketPair testSockets = new SocketPair(ProtocolType.Udp, SocketType.Stream); }); @@ -183,7 +183,7 @@ public void SocketExceptionTest18_Fault() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { testSockets.socketClient.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, new byte[] { (byte)0 }); @@ -197,7 +197,7 @@ public void SocketExceptionTest18_Fault() public void SocketExceptionTest19_ProtocolOption() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { testSockets.Startup(0, 0); testSockets.socketClient.GetSocketOption(SocketOptionLevel.IP, @@ -212,7 +212,7 @@ public void SocketExceptionTest19_ProtocolOption() public void SocketExceptionTest20_OperationNotSupported() { SocketPair testSockets = new SocketPair(ProtocolType.Tcp, SocketType.Stream); - Assert.Throws(typeof(SocketException), () => + Assert.ThrowsException(typeof(SocketException), () => { testSockets.Startup(0, 0); diff --git a/Tests/SocketTests/SocketOptionsTests.cs b/Tests/SocketTests/SocketOptionsTests.cs index 80b455d..953d399 100644 --- a/Tests/SocketTests/SocketOptionsTests.cs +++ b/Tests/SocketTests/SocketOptionsTests.cs @@ -29,17 +29,17 @@ public void SocketGetSocketOptions_00() socketType, ProtocolType.Tcp); - Assert.Throws(typeof(NotSupportedException), () => + Assert.ThrowsException(typeof(NotSupportedException), () => { testSocket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.AddMembership); }, "Getting SocketOptionName.AddMembership should have thrown an exception"); - Assert.Throws(typeof(NotSupportedException), () => + Assert.ThrowsException(typeof(NotSupportedException), () => { testSocket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DropMembership); }, "Getting SocketOptionName.DropMembership should have thrown an exception"); - Assert.True((SocketType)testSocket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Type) == socketType, "Getting SocketOptionName.Type returned a different type."); + Assert.IsTrue((SocketType)testSocket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Type) == socketType, "Getting SocketOptionName.Type returned a different type."); testSocket?.Close(); } diff --git a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs index 13a5f29..732f0e2 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs @@ -28,7 +28,7 @@ public static class NetworkHelper private static IPConfiguration _ipConfiguration; /// - /// 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 +37,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 +60,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) { @@ -73,6 +78,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, @@ -89,6 +95,7 @@ public static void SetupNetworkHelper( /// /// 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 +109,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 +130,28 @@ 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; + + _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,7 +224,7 @@ internal static bool InternalWaitNetworkAvailable( private static void WorkingThread() { // check if we have an IP - if(!NetworkHelperInternal.CheckIP( + if (!NetworkHelperInternal.CheckIP( _workingNetworkInterface, _ipConfiguration)) { @@ -217,68 +247,68 @@ private static void WorkingThread() private static void AddressChangedCallback(object sender, EventArgs e) { - if(NetworkHelperInternal.CheckIP( + 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; + _networkHelperStatus = NetworkHelperStatus.FailedNoNetworkInterface; - throw new NotSupportedException(); - } - - // 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..acac8e9 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs @@ -42,6 +42,11 @@ public enum NetworkHelperStatus /// ErrorConnetingToWiFiWhileScanning, + /// + /// The network was previously ready but the IP address was lost. Waiting for the connection to be restored. + /// + Reconnecting, + /// /// An exception occurred with waiting for the network to become ready. Check HelperException property to find the that was thrown. /// From 338aab39165248b7f8be637eed2ed130b754a908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Fri, 29 May 2026 11:23:12 +0100 Subject: [PATCH 2/2] Fixes from code review --- README.md | 2 +- .../ConnectToEthernetTests.cs | 6 ++- .../NetworkHelper/NetworkHelper.cs | 44 +++++++++++++++++-- .../NetworkHelper/NetworkHelperStatus.cs | 8 ++-- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a547f4a..156c3ee 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ 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 c186cc6..7a55754 100644 --- a/Tests/NetworkHelperTests/ConnectToEthernetTests.cs +++ b/Tests/NetworkHelperTests/ConnectToEthernetTests.cs @@ -107,7 +107,11 @@ public void TestRetryAfterTimeout() // 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.None); + Assert.IsTrue( + NetworkHelper.Status == NetworkHelperStatus.NetworkIsReady || + NetworkHelper.Status == NetworkHelperStatus.TokenExpiredWaitingIPAddress || + NetworkHelper.Status == NetworkHelperStatus.TokenExpiredWaitingDateTime, + "Expected a terminal status after the second attempt"); NetworkHelper.Reset(); } diff --git a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs index 732f0e2..04696d3 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelper.cs @@ -27,6 +27,9 @@ 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 one call to the event-based helper methods. /// @@ -69,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(); } /// @@ -90,7 +94,8 @@ public static void SetupNetworkHelper( SetupHelper(true); // fire working thread - new Thread(WorkingThread).Start(); + _workerThread = new Thread(WorkingThread); + _workerThread.Start(); } /// @@ -143,6 +148,22 @@ 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); @@ -228,16 +249,28 @@ private static void WorkingThread() _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(); @@ -247,6 +280,11 @@ private static void WorkingThread() private static void AddressChangedCallback(object sender, EventArgs e) { + if (_stopRequested) + { + return; + } + if (NetworkHelperInternal.CheckIP( _workingNetworkInterface, _ipConfiguration)) diff --git a/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs b/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs index acac8e9..57f7895 100644 --- a/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs +++ b/nanoFramework.System.Net/NetworkHelper/NetworkHelperStatus.cs @@ -43,13 +43,13 @@ public enum NetworkHelperStatus ErrorConnetingToWiFiWhileScanning, /// - /// The network was previously ready but the IP address was lost. Waiting for the connection to be restored. + /// An exception occurred with waiting for the network to become ready. Check HelperException property to find the that was thrown. /// - Reconnecting, + ExceptionOccurred, /// - /// An exception occurred with waiting for the network to become ready. Check HelperException property to find the that was thrown. + /// The network was previously ready but the IP address was lost. Waiting for the connection to be restored. /// - ExceptionOccurred + Reconnecting } }