diff --git a/libsecondlife-cs/NetworkManager.cs b/libsecondlife-cs/NetworkManager.cs index 632309f7..42e4b0a9 100644 --- a/libsecondlife-cs/NetworkManager.cs +++ b/libsecondlife-cs/NetworkManager.cs @@ -366,21 +366,35 @@ namespace libsecondlife private void OnReceivedData(IAsyncResult result) { - Packet packet; + Packet packet = null; + int numBytes; // If we're receiving data the sim connection is open connected = true; + #region RecvBufferMutex try { - #region RecvBufferMutex RecvBufferMutex.WaitOne(); // Update the disconnect flag so this sim doesn't time out DisconnectCandidate = false; // Retrieve the incoming packet - int numBytes = Connection.EndReceiveFrom(result, ref endPoint); + try + { + numBytes = Connection.EndReceiveFrom(result, ref endPoint); + } + catch (SocketException) + { + Client.Log("Socket error, shutting down the " + this.Region.Name + " sim", Helpers.LogLevel.Warning); + + connected = false; + RecvBufferMutex.ReleaseMutex(); + Network.DisconnectSim(this); + + return; + } if ((RecvBuffer[0] & Helpers.MSG_APPENDED_ACKS) != 0) { @@ -390,13 +404,24 @@ namespace libsecondlife Client.Log("Found " + numAcks + " appended acks", Helpers.LogLevel.Info); #region NeedAckMutex - NeedAckMutex.WaitOne(); - for (int i = 1; i <= numAcks; ++i) + try { - ushort ack = (ushort)BitConverter.ToUInt32(RecvBuffer, (numBytes - i * 4) - 1); - NeedAck.Remove(ack); + NeedAckMutex.WaitOne(); + + for (int i = 1; i <= numAcks; ++i) + { + ushort ack = (ushort)BitConverter.ToUInt32(RecvBuffer, (numBytes - i * 4) - 1); + NeedAck.Remove(ack); + } + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + NeedAckMutex.ReleaseMutex(); } - NeedAckMutex.ReleaseMutex(); #endregion NeedAckMutex // Adjust the packet length @@ -421,27 +446,43 @@ namespace libsecondlife // Start listening again since we're done with RecvBuffer Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); - + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { RecvBufferMutex.ReleaseMutex(); - #endregion RecvBufferMutex + } + #endregion RecvBufferMutex - if (packet.Layout.Name == "") + // Fail-safe checks + if (packet == null) + { + Client.Log("OnReceivedData() fail-safe reached, exiting", Helpers.LogLevel.Warning); + return; + } + else if (packet.Layout.Name == "") + { + // TODO: Add a packet dump function to Packet and dump the raw data here + Client.Log("Received an unrecognized packet", Helpers.LogLevel.Warning); + return; + } + + // Track the sequence number for this packet if it's marked as reliable + if ((packet.Data[0] & Helpers.MSG_RELIABLE) != 0) + { + // Send the ACK for this packet + // TODO: If we can make it stable, go back to the periodic ACK system + SendACK((uint)packet.Sequence); + + // Check if we already received this packet + #region InboxMutex + try { - // TODO: Add a packet dump function to Packet and dump the raw data here - Client.Log("Received an unrecognized packet", Helpers.LogLevel.Warning); - return; - } - - // Track the sequence number for this packet if it's marked as reliable - if ((packet.Data[0] & Helpers.MSG_RELIABLE) != 0) - { - // Send the ACK for this packet - // TODO: If we can make it stable, go back to the periodic ACK system - SendACK((uint)packet.Sequence); - - // Check if we already received this packet - #region InboxMutex InboxMutex.WaitOne(); + if (Inbox.Contains(packet.Sequence)) { Client.Log("Received a duplicate " + packet.Layout.Name + ", sequence=" + @@ -450,39 +491,53 @@ namespace libsecondlife Helpers.LogLevel.Info); // Avoid firing a callback twice for the same packet - InboxMutex.ReleaseMutex(); return; } else { Inbox.Add(packet.Sequence, packet.Sequence); } - #endregion InboxMutex + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { InboxMutex.ReleaseMutex(); } + #endregion InboxMutex + } - if (packet.Layout.Name == null) - { - Client.Log("Received an unrecognized packet", Helpers.LogLevel.Warning); - return; - } - else if (packet.Layout.Name == "PacketAck") - { - // PacketAck is handled directly instead of using a callback to simplify access to - // the NeedAck hashtable and its mutex - ArrayList blocks = packet.Blocks(); + if (packet.Layout.Name == "PacketAck") + { + // PacketAck is handled directly instead of using a callback to simplify access to + // the NeedAck hashtable and its mutex + ArrayList blocks = packet.Blocks(); - #region NeedAckMutex + #region NeedAckMutex + try + { NeedAckMutex.WaitOne(); foreach (Block block in blocks) { Field ID = (Field)block.Fields[0]; NeedAck.Remove((ushort)(uint)ID.Data); } - NeedAckMutex.ReleaseMutex(); - #endregion NeedAckMutex } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + NeedAckMutex.ReleaseMutex(); + } + #endregion NeedAckMutex + } + try + { if (Callbacks.ContainsKey(packet.Layout.Name)) { ArrayList callbackArray = (ArrayList)Callbacks[packet.Layout.Name]; @@ -509,20 +564,14 @@ namespace libsecondlife } } } - - // Erase this packet from memory - packet = null; - } - catch (SocketException) - { - Client.Log("Socket error, shutting down the " + this.Region.Name + " sim", Helpers.LogLevel.Warning); - Network.DisconnectSim(this); } catch (Exception e) { - Client.Log(e.ToString(), Helpers.LogLevel.Error); - Client.Log("One or more mutexes may be deadlocked", Helpers.LogLevel.Warning); + Client.Log("Caught an exception in a packet callback: " + e.ToString(), Helpers.LogLevel.Warning); } + + // Erase this packet from memory + packet = null; } } @@ -598,7 +647,7 @@ namespace libsecondlife Client = client; Protocol = protocol; Simulators = new ArrayList(); - SimulatorsMutex = new Mutex(false, "SimulatorsMutex"); + SimulatorsMutex = new Mutex(true, "SimulatorsMutex"); Callbacks = new Hashtable(); CurrentSim = null; LoginValues = null; @@ -839,11 +888,10 @@ namespace libsecondlife // Set the current region Client.CurrentRegion = simulator.Region; - // Simulator is successfully connected, add it to the list and set it as default - SimulatorsMutex.WaitOne(); + // Simulator is successfully connected, add it to the list and set it as default Simulators.Add(simulator); - SimulatorsMutex.ReleaseMutex(); - CurrentSim = simulator; + + CurrentSim = simulator; // Move our agent in to the sim to complete the connection Packet packet = Packets.Sim.CompleteAgentMovement(Protocol, AgentID, SessionID, circuitCode); @@ -865,9 +913,19 @@ namespace libsecondlife } #region SimulatorsMutex - SimulatorsMutex.WaitOne(); - Simulators.Add(simulator); - SimulatorsMutex.ReleaseMutex(); + try + { + SimulatorsMutex.WaitOne(); + Simulators.Add(simulator); + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + SimulatorsMutex.ReleaseMutex(); + } #endregion SimulatorsMutex if (setDefault) @@ -917,9 +975,19 @@ namespace libsecondlife } #region SimulatorsMutex - SimulatorsMutex.WaitOne(); - Simulators.Remove(sim); - SimulatorsMutex.ReleaseMutex(); + try + { + SimulatorsMutex.WaitOne(); + Simulators.Remove(sim); + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + SimulatorsMutex.ReleaseMutex(); + } #endregion SimulatorsMutex } @@ -930,84 +998,102 @@ namespace libsecondlife private void Shutdown() { #region SimulatorsMutex - SimulatorsMutex.WaitOne(); - - // Disconnect all simulators except the current one - foreach (Simulator simulator in Simulators) + try { - // Don't disconnect the current sim, we'll use LogoutRequest for that - if (simulator != CurrentSim) - { - simulator.Disconnect(); + SimulatorsMutex.WaitOne(); - // Fire the SimDisconnected event if a handler is registered - if (OnSimDisconnected != null) + // Disconnect all simulators except the current one + foreach (Simulator simulator in Simulators) + { + // Don't disconnect the current sim, we'll use LogoutRequest for that + if (simulator != CurrentSim) { - OnSimDisconnected(simulator, DisconnectType.NetworkTimeout); + simulator.Disconnect(); + + // Fire the SimDisconnected event if a handler is registered + if (OnSimDisconnected != null) + { + OnSimDisconnected(simulator, DisconnectType.NetworkTimeout); + } } } } - - Simulators.Clear(); - SimulatorsMutex.ReleaseMutex(); + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + Simulators.Clear(); + SimulatorsMutex.ReleaseMutex(); + } #endregion SimulatorsMutex CurrentSim.Disconnect(); CurrentSim = null; } - private void DisconnectTimer_Elapsed(object sender, ElapsedEventArgs e) + private void DisconnectTimer_Elapsed(object sender, ElapsedEventArgs ev) { #region SimulatorsMutex - SimulatorsMutex.WaitOne(); - - Beginning: - - foreach (Simulator sim in Simulators) + try { - if (sim.DisconnectCandidate == true) + SimulatorsMutex.WaitOne(); + + Beginning: + + foreach (Simulator sim in Simulators) { - if (sim == CurrentSim) + if (sim.DisconnectCandidate == true) { - Client.Log("Network timeout for the current simulator (" + - sim.Region.Name + "), logging out", Helpers.LogLevel.Warning); - - DisconnectTimer.Stop(); - connected = false; - - // Shutdown the network layer - Shutdown(); - - if (OnDisconnected != null) + if (sim == CurrentSim) { - OnDisconnected(DisconnectType.NetworkTimeout, ""); - } + Client.Log("Network timeout for the current simulator (" + + sim.Region.Name + "), logging out", Helpers.LogLevel.Warning); - // We're completely logged out and shut down, leave this function - return; + DisconnectTimer.Stop(); + connected = false; + + // Shutdown the network layer + Shutdown(); + + if (OnDisconnected != null) + { + OnDisconnected(DisconnectType.NetworkTimeout, ""); + } + + // We're completely logged out and shut down, leave this function + return; + } + else + { + // This sim hasn't received any network traffic since the + // timer last elapsed, consider it disconnected + Client.Log("Network timeout for simulator " + sim.Region.Name + + ", disconnecting", Helpers.LogLevel.Warning); + + SimulatorsMutex.ReleaseMutex(); + DisconnectSim(sim); + SimulatorsMutex.WaitOne(); + + // Reset the iterator since we removed an element + goto Beginning; + } } else { - // This sim hasn't received any network traffic since the - // timer last elapsed, consider it disconnected - Client.Log("Network timeout for simulator " + sim.Region.Name + - ", disconnecting", Helpers.LogLevel.Warning); - - SimulatorsMutex.ReleaseMutex(); - DisconnectSim(sim); - SimulatorsMutex.WaitOne(); - - // Reset the iterator since we removed an element - goto Beginning; + sim.DisconnectCandidate = true; } } - else - { - sim.DisconnectCandidate = true; - } } - - SimulatorsMutex.ReleaseMutex(); + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + finally + { + SimulatorsMutex.ReleaseMutex(); + } #endregion SimulatorsMutex } diff --git a/libsecondlife-cs/tests/DebugServer.cs b/libsecondlife-cs/tests/DebugServer.cs new file mode 100644 index 00000000..be13194c --- /dev/null +++ b/libsecondlife-cs/tests/DebugServer.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using Nwc.XmlRpc; +using libsecondlife; + +namespace libsecondlife.Tests +{ + public class DebugServer + { + public bool Initialized = false; + + private SecondLife libsl; + private bool done = false; + private Socket Listener; + private IPEndPoint Endpoint; + EndPoint RemoteEndpoint = new IPEndPoint(IPAddress.Loopback, 0); + private ushort Sequence = 0; + + public DebugServer(string keywordFile, string mapFile, int port) + { + try + { + libsl = new SecondLife(keywordFile, mapFile); + } + catch (Exception) + { + return; + } + + BindSocket(port); + } + + private void BindSocket(int port) + { + Endpoint = new IPEndPoint(IPAddress.Loopback, port); + Listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + Console.WriteLine("[SERVER] Binding a UDP socket to " + Endpoint.ToString()); + + try + { + Listener.Bind(Endpoint); + } + catch (SocketException) + { + Console.WriteLine("[SERVER] Failed to bind to " + Endpoint.ToString()); + return; + } + + // Start listening for incoming data + Thread thread = new Thread(new ThreadStart(Listen)); + thread.Start(); + + Initialized = true; + } + + private void Listen() + { + Packet packet; + int length; + byte[] bytes = new byte[4096]; + + Console.WriteLine("[SERVER] Listening for incoming data on " + Endpoint.ToString()); + + while (!done) + { + packet = null; + + // Grab the next packet + length = Listener.ReceiveFrom(bytes, ref RemoteEndpoint); + + Console.WriteLine("[SERVER] Received a packet from {0}", RemoteEndpoint.ToString()); + + if (System.Text.ASCIIEncoding.UTF8.GetString(bytes).Substring(0, 10) == "stopserver") + { + Console.WriteLine("[SERVER] Received a shutdown request, stopping the server"); + done = true; + break; + } + + if ((bytes[0] & Helpers.MSG_APPENDED_ACKS) != 0) + { + byte numAcks = bytes[length - 1]; + + Console.WriteLine("[SERVER] Found " + numAcks + " appended acks"); + + length = (length - numAcks * 4) - 1; + } + + if ((bytes[0] & Helpers.MSG_ZEROCODED) != 0) + { + // Allocate a temporary buffer for the zerocoded packet + byte[] zeroBuffer = new byte[4096]; + int zeroBytes = Helpers.ZeroDecode(bytes, length, zeroBuffer); + length = zeroBytes; + packet = new Packet(zeroBuffer, length, libsl.Protocol); + } + else + { + // Create the packet object from our byte array + packet = new Packet(bytes, length, libsl.Protocol); + } + + if ((packet.Data[0] & Helpers.MSG_RELIABLE) != 0) + { + SendACK((uint)packet.Sequence); + } + + Console.WriteLine(packet.ToString()); + } + + Console.WriteLine("[SERVER] Shutting down the socket on " + Endpoint.ToString()); + Listener.Close(); + } + + private void SendACK(uint id) + { + try + { + Packet packet = new Packet("PacketAck", libsl.Protocol, 13); + packet.Data[8] = 1; + Array.Copy(BitConverter.GetBytes(id), 0, packet.Data, 9, 4); + + // Set the sequence number + packet.Sequence = ++Sequence; + + Listener.SendTo(packet.Data, RemoteEndpoint); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + } +} diff --git a/libsecondlife-cs/tests/Tests.cs b/libsecondlife-cs/tests/Tests.cs new file mode 100644 index 00000000..89fe0877 --- /dev/null +++ b/libsecondlife-cs/tests/Tests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Net; +using libsecondlife; +using NUnit.Framework; + +namespace libsecondlife.Tests +{ + [TestFixture] + public class EndianTests : Assert + { + SecondLife Client; + DebugServer Server; + + [SetUp] + public void Init() + { + // Initialize the client + try + { + Client = new SecondLife("keywords.txt", "message_template.msg"); + Client.Network.AgentID = LLUUID.GenerateUUID(); + Client.Network.SessionID = LLUUID.GenerateUUID(); + } + catch (Exception) + { + Assert.IsTrue(false, "Failed to initialize the client, " + + "keywords.txt or message_template.msg missing?"); + } + + Console.WriteLine("Initializing the server"); + Server = new DebugServer("keywords.txt", "message_template.msg", 8338); + + Assert.IsTrue(Server.Initialized, "Failed to initialize the server, couldn't bind to port 8338?"); + + // Login with the client to the server + Simulator sim = Client.Network.Connect(IPAddress.Loopback, 8338, 1, true); + + Assert.IsNotNull(sim, "Failed to connect to the debugging simulator"); + } + + private void StartServer() + { + Console.WriteLine("Initializing the server"); + Server = new DebugServer("keywords.txt", "message_template.msg", 8338); + } + + [Test] + public void SimpleTest() + { + Assert.IsTrue(true); + } + + [TearDown] + public void Shutdown() + { + // Log the client out + try + { + // Shutdown the server + Client.Network.SendPacket(System.Text.Encoding.UTF8.GetBytes("stopserver")); + + Client.Network.Logout(); + } + catch (NotConnectedException) + { + Assert.IsTrue(false, "Logout failed, not connected"); + } + + Client = null; + } + } +}