Create FunSLUDP client stack. At the moment it's only a copy of the LindenUDP
client stack.0.6.0-stable
parent
d367f0bf0c
commit
f180fa57e7
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public interface LLClientStackNetworkHandler
|
||||
{
|
||||
void SendPacketTo(byte[] buffer, int size, SocketFlags flags, uint circuitcode); // EndPoint packetSender);
|
||||
void RemoveClientCircuit(uint circuitcode);
|
||||
void RegisterPacketServer(LLPacketServer server);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,702 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Timers;
|
||||
using libsecondlife;
|
||||
using libsecondlife.Packets;
|
||||
using Timer = System.Timers.Timer;
|
||||
using OpenSim.Framework;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public delegate void PacketStats(int inPackets, int outPackets, int unAckedBytes);
|
||||
public delegate void PacketDrop(Packet pack, Object id);
|
||||
public delegate bool SynchronizeClientHandler(IScene scene, Packet packet, LLUUID agentID, ThrottleOutPacketType throttlePacketType);
|
||||
|
||||
public interface IPacketHandler
|
||||
{
|
||||
event PacketStats OnPacketStats;
|
||||
event PacketDrop OnPacketDrop;
|
||||
SynchronizeClientHandler SynchronizeClient { set; }
|
||||
|
||||
int PacketsReceived { get; }
|
||||
int PacketsReceivedReported { get; }
|
||||
uint SilenceLimit { get; set; }
|
||||
uint DiscardTimeout { get; set; }
|
||||
uint ResendTimeout { get; set; }
|
||||
|
||||
void InPacket(Packet packet);
|
||||
void ProcessInPacket(Packet packet);
|
||||
void OutPacket(Packet NewPack,
|
||||
ThrottleOutPacketType throttlePacketType);
|
||||
void OutPacket(Packet NewPack,
|
||||
ThrottleOutPacketType throttlePacketType, Object id);
|
||||
LLPacketQueue PacketQueue { get; }
|
||||
void Stop();
|
||||
void Flush();
|
||||
void Clear();
|
||||
ClientInfo GetClientInfo();
|
||||
void SetClientInfo(ClientInfo info);
|
||||
void AddImportantPacket(PacketType type);
|
||||
void RemoveImportantPacket(PacketType type);
|
||||
}
|
||||
|
||||
public class LLPacketHandler : IPacketHandler
|
||||
{
|
||||
// Packet queues
|
||||
//
|
||||
LLPacketQueue m_PacketQueue;
|
||||
|
||||
public LLPacketQueue PacketQueue
|
||||
{
|
||||
get { return m_PacketQueue; }
|
||||
}
|
||||
|
||||
// Timer to run stats and acks on
|
||||
//
|
||||
private Timer m_AckTimer = new Timer(250);
|
||||
|
||||
// A list of the packets we haven't acked yet
|
||||
//
|
||||
private Dictionary<uint,uint> m_PendingAcks = new Dictionary<uint,uint>();
|
||||
// Dictionary of the packets that need acks from the client.
|
||||
//
|
||||
private class AckData
|
||||
{
|
||||
public AckData(Packet packet, Object identifier)
|
||||
{
|
||||
Packet = packet;
|
||||
Identifier = identifier;
|
||||
}
|
||||
|
||||
public Packet Packet;
|
||||
public Object Identifier;
|
||||
}
|
||||
private Dictionary<uint, AckData> m_NeedAck =
|
||||
new Dictionary<uint, AckData>();
|
||||
|
||||
private uint m_ResendTimeout = 1000;
|
||||
|
||||
public uint ResendTimeout
|
||||
{
|
||||
get { return m_ResendTimeout; }
|
||||
set { m_ResendTimeout = value; }
|
||||
}
|
||||
|
||||
private uint m_DiscardTimeout = 8000;
|
||||
|
||||
public uint DiscardTimeout
|
||||
{
|
||||
get { return m_DiscardTimeout; }
|
||||
set { m_DiscardTimeout = value; }
|
||||
}
|
||||
|
||||
private uint m_SilenceLimit = 250;
|
||||
|
||||
public uint SilenceLimit
|
||||
{
|
||||
get { return m_SilenceLimit; }
|
||||
set { m_SilenceLimit = value; }
|
||||
}
|
||||
|
||||
private int m_LastAck = 0;
|
||||
|
||||
// Track duplicated packets. This uses a Dictionary. Both insertion
|
||||
// and lookup are common operations and need to take advantage of
|
||||
// the hashing. Expiration is less common and can be allowed the
|
||||
// time for a linear scan.
|
||||
//
|
||||
private Dictionary<uint, int> m_DupeTracker =
|
||||
new Dictionary<uint, int>();
|
||||
//private uint m_DupeTrackerWindow = 30;
|
||||
|
||||
// Values for the SimStatsReporter
|
||||
//
|
||||
private int m_PacketsReceived = 0;
|
||||
private int m_PacketsReceivedReported = 0;
|
||||
private int m_PacketsSent = 0;
|
||||
private int m_PacketsSentReported = 0;
|
||||
private int m_UnackedBytes = 0;
|
||||
|
||||
public int PacketsReceived
|
||||
{
|
||||
get { return m_PacketsReceived; }
|
||||
}
|
||||
|
||||
public int PacketsReceivedReported
|
||||
{
|
||||
get { return m_PacketsReceivedReported; }
|
||||
}
|
||||
|
||||
// The client we are working for
|
||||
//
|
||||
private IClientAPI m_Client;
|
||||
|
||||
// Some events
|
||||
//
|
||||
public event PacketStats OnPacketStats;
|
||||
public event PacketDrop OnPacketDrop;
|
||||
|
||||
private SynchronizeClientHandler m_SynchronizeClient = null;
|
||||
|
||||
public SynchronizeClientHandler SynchronizeClient
|
||||
{
|
||||
set { m_SynchronizeClient = value; }
|
||||
}
|
||||
|
||||
// Packet sequencing
|
||||
//
|
||||
private uint m_Sequence = 0;
|
||||
private object m_SequenceLock = new object();
|
||||
private const int MAX_SEQUENCE = 0xFFFFFF;
|
||||
|
||||
List<PacketType> m_ImportantPackets = new List<PacketType>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Constructors
|
||||
//
|
||||
public LLPacketHandler(IClientAPI client)
|
||||
{
|
||||
m_Client = client;
|
||||
|
||||
m_PacketQueue = new LLPacketQueue(client.AgentId);
|
||||
|
||||
m_AckTimer.Elapsed += AckTimerElapsed;
|
||||
m_AckTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
m_AckTimer.Stop();
|
||||
|
||||
m_PacketQueue.Enqueue(null);
|
||||
}
|
||||
|
||||
// Send one packet. This actually doesn't send anything, it queues
|
||||
// it. Designed to be fire-and-forget, but there is an optional
|
||||
// notifier.
|
||||
//
|
||||
public void OutPacket(
|
||||
Packet packet, ThrottleOutPacketType throttlePacketType)
|
||||
{
|
||||
OutPacket(packet, throttlePacketType, null);
|
||||
}
|
||||
|
||||
public void OutPacket(
|
||||
Packet packet, ThrottleOutPacketType throttlePacketType,
|
||||
Object id)
|
||||
{
|
||||
// Call the load balancer's hook. If this is not active here
|
||||
// we defer to the sim server this client is actually connected
|
||||
// to. Packet drop notifies will not be triggered in this
|
||||
// configuration!
|
||||
//
|
||||
if ((m_SynchronizeClient != null) && (!m_Client.IsActive))
|
||||
{
|
||||
if (m_SynchronizeClient(m_Client.Scene, packet,
|
||||
m_Client.AgentId, throttlePacketType))
|
||||
return;
|
||||
}
|
||||
|
||||
packet.Header.Sequence = NextPacketSequenceNumber();
|
||||
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
DropResend(id);
|
||||
|
||||
AddAcks(ref packet);
|
||||
QueuePacket(packet, throttlePacketType, id);
|
||||
|
||||
// We want to see that packet arrive if it's reliable
|
||||
if (packet.Header.Reliable)
|
||||
{
|
||||
m_UnackedBytes += packet.ToBytes().Length;
|
||||
m_NeedAck[packet.Header.Sequence] = new AckData(packet, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAcks(ref Packet packet)
|
||||
{
|
||||
// This packet type has shown to have issues with
|
||||
// acks being appended to the payload, just don't send
|
||||
// any with this packet type until libsl is fixed.
|
||||
//
|
||||
if(packet is libsecondlife.Packets.ViewerEffectPacket)
|
||||
return;
|
||||
|
||||
// Add acks to outgoing packets
|
||||
//
|
||||
if (m_PendingAcks.Count > 0)
|
||||
{
|
||||
int count = m_PendingAcks.Count;
|
||||
if (count > 10)
|
||||
count = 10;
|
||||
packet.Header.AckList = new uint[count];
|
||||
packet.Header.AppendedAcks = true;
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (uint ack in new List<uint>(m_PendingAcks.Keys))
|
||||
{
|
||||
packet.Header.AckList[i] = ack;
|
||||
i++;
|
||||
m_PendingAcks.Remove(ack);
|
||||
if (i >= count) // That is how much space there is
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueuePacket(
|
||||
Packet packet, ThrottleOutPacketType throttlePacketType,
|
||||
Object id)
|
||||
{
|
||||
packet.TickCount = System.Environment.TickCount;
|
||||
|
||||
LLQueItem item = new LLQueItem();
|
||||
item.Packet = packet;
|
||||
item.Incoming = false;
|
||||
item.throttleType = throttlePacketType;
|
||||
item.Identifier = id;
|
||||
|
||||
m_PacketQueue.Enqueue(item);
|
||||
m_PacketsSent++;
|
||||
}
|
||||
|
||||
private void ResendUnacked()
|
||||
{
|
||||
int now = System.Environment.TickCount;
|
||||
int lastAck = m_LastAck;
|
||||
|
||||
// Unless we have received at least one ack, don't bother resending
|
||||
// anything. There may not be a client there, don't clog up the
|
||||
// pipes.
|
||||
//
|
||||
if (lastAck == 0)
|
||||
return;
|
||||
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
// Nothing to do
|
||||
//
|
||||
if (m_NeedAck.Count == 0)
|
||||
return;
|
||||
|
||||
// If we have seen no acks in <SilenceLimit> s but are
|
||||
// waiting for acks, then there may be no one listening.
|
||||
// No need to resend anything. Keep it until it gets stale,
|
||||
// then it will be dropped.
|
||||
//
|
||||
if ((((now - lastAck) > m_SilenceLimit) &&
|
||||
m_NeedAck.Count > 0) || m_NeedAck.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (AckData data in new List<AckData>(m_NeedAck.Values))
|
||||
{
|
||||
Packet packet = data.Packet;
|
||||
|
||||
// Packets this old get resent
|
||||
//
|
||||
if ((now - packet.TickCount) > m_ResendTimeout)
|
||||
{
|
||||
// Resend the packet. Set the packet's tick count to
|
||||
// now, and keep it marked as resent.
|
||||
//
|
||||
packet.Header.Resent = true;
|
||||
QueuePacket(packet, ThrottleOutPacketType.Resend,
|
||||
data.Identifier);
|
||||
}
|
||||
|
||||
// The discard logic
|
||||
// If the packet is in the queue for <DiscardTimeout> s
|
||||
// without having been processed, then we have clogged
|
||||
// pipes. Most likely, the client is gone
|
||||
// Drop the packets
|
||||
//
|
||||
if ((now - packet.TickCount) > m_DiscardTimeout)
|
||||
{
|
||||
if (!m_ImportantPackets.Contains(packet.Type))
|
||||
m_NeedAck.Remove(packet.Header.Sequence);
|
||||
|
||||
TriggerOnPacketDrop(packet, data.Identifier);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send the pending packet acks to the client
|
||||
// Will send blocks of acks for up to 250 packets
|
||||
//
|
||||
private void SendAcks()
|
||||
{
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
if (m_PendingAcks.Count == 0)
|
||||
return;
|
||||
|
||||
PacketAckPacket acks = (PacketAckPacket)PacketPool.Instance.GetPacket(PacketType.PacketAck);
|
||||
|
||||
// The case of equality is more common than one might think,
|
||||
// because this function will be called unconditionally when
|
||||
// the counter reaches 250. So there is a good chance another
|
||||
// packet with 250 blocks exists.
|
||||
//
|
||||
if (acks.Packets == null ||
|
||||
acks.Packets.Length != m_PendingAcks.Count)
|
||||
acks.Packets = new PacketAckPacket.PacketsBlock[m_PendingAcks.Count];
|
||||
int i = 0;
|
||||
foreach (uint ack in new List<uint>(m_PendingAcks.Keys))
|
||||
{
|
||||
acks.Packets[i] = new PacketAckPacket.PacketsBlock();
|
||||
acks.Packets[i].ID = ack;
|
||||
|
||||
m_PendingAcks.Remove(ack);
|
||||
i++;
|
||||
}
|
||||
|
||||
acks.Header.Reliable = false;
|
||||
OutPacket(acks, ThrottleOutPacketType.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
// Queue a packet ack. It will be sent either after 250 acks are
|
||||
// queued, or when the timer fires.
|
||||
//
|
||||
private void AckPacket(Packet packet)
|
||||
{
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
if (m_PendingAcks.Count < 250)
|
||||
{
|
||||
if (!m_PendingAcks.ContainsKey(packet.Header.Sequence))
|
||||
m_PendingAcks.Add(packet.Header.Sequence,
|
||||
packet.Header.Sequence);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SendAcks();
|
||||
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
// If this is still full we have a truly exceptional
|
||||
// condition (means, can't happen)
|
||||
//
|
||||
if (m_PendingAcks.Count < 250)
|
||||
{
|
||||
if (!m_PendingAcks.ContainsKey(packet.Header.Sequence))
|
||||
m_PendingAcks.Add(packet.Header.Sequence,
|
||||
packet.Header.Sequence);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the timer elapses, send the pending acks, trigger resends
|
||||
// and report all the stats.
|
||||
//
|
||||
private void AckTimerElapsed(object sender, ElapsedEventArgs ea)
|
||||
{
|
||||
SendAcks();
|
||||
ResendUnacked();
|
||||
SendPacketStats();
|
||||
}
|
||||
|
||||
// Push out pachet counts for the sim status reporter
|
||||
//
|
||||
private void SendPacketStats()
|
||||
{
|
||||
PacketStats handlerPacketStats = OnPacketStats;
|
||||
if (handlerPacketStats != null)
|
||||
{
|
||||
handlerPacketStats(
|
||||
m_PacketsReceived - m_PacketsReceivedReported,
|
||||
m_PacketsSent - m_PacketsSentReported,
|
||||
m_UnackedBytes);
|
||||
|
||||
m_PacketsReceivedReported = m_PacketsReceived;
|
||||
m_PacketsSentReported = m_PacketsSent;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't keep an unlimited record of dupes. This will prune the
|
||||
// dictionary by age.
|
||||
//
|
||||
// private void PruneDupeTracker()
|
||||
// {
|
||||
// lock (m_DupeTracker)
|
||||
// {
|
||||
// Dictionary<uint, int> packs =
|
||||
// new Dictionary<uint, int>(m_DupeTracker);
|
||||
//
|
||||
// foreach (uint pack in packs.Keys)
|
||||
// {
|
||||
// if (Util.UnixTimeSinceEpoch() - m_DupeTracker[pack] >
|
||||
// m_DupeTrackerWindow)
|
||||
// m_DupeTracker.Remove(pack);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
public void InPacket(Packet packet)
|
||||
{
|
||||
if (packet == null)
|
||||
return;
|
||||
|
||||
// If this client is on another partial instance, no need
|
||||
// to handle packets
|
||||
//
|
||||
if (!m_Client.IsActive && packet.Type != PacketType.LogoutRequest)
|
||||
{
|
||||
PacketPool.Instance.ReturnPacket(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Any packet can have some packet acks in the header.
|
||||
// Process them here
|
||||
//
|
||||
if (packet.Header.AppendedAcks)
|
||||
{
|
||||
foreach (uint id in packet.Header.AckList)
|
||||
{
|
||||
ProcessAck(id);
|
||||
}
|
||||
}
|
||||
|
||||
// When too many acks are needed to be sent, the client sends
|
||||
// a packet consisting of acks only
|
||||
//
|
||||
if (packet.Type == PacketType.PacketAck)
|
||||
{
|
||||
PacketAckPacket ackPacket = (PacketAckPacket)packet;
|
||||
|
||||
foreach (PacketAckPacket.PacketsBlock block in
|
||||
ackPacket.Packets)
|
||||
{
|
||||
ProcessAck(block.ID);
|
||||
}
|
||||
|
||||
PacketPool.Instance.ReturnPacket(packet);
|
||||
return;
|
||||
}
|
||||
else if (packet.Type == PacketType.StartPingCheck)
|
||||
{
|
||||
StartPingCheckPacket startPing = (StartPingCheckPacket)packet;
|
||||
CompletePingCheckPacket endPing = (CompletePingCheckPacket)PacketPool.Instance.GetPacket(PacketType.CompletePingCheck);
|
||||
|
||||
endPing.PingID.PingID = startPing.PingID.PingID;
|
||||
OutPacket(endPing, ThrottleOutPacketType.Task);
|
||||
}
|
||||
else
|
||||
{
|
||||
LLQueItem item = new LLQueItem();
|
||||
item.Packet = packet;
|
||||
item.Incoming = true;
|
||||
m_PacketQueue.Enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessInPacket(Packet packet)
|
||||
{
|
||||
// Always ack the packet!
|
||||
//
|
||||
if (packet.Header.Reliable)
|
||||
AckPacket(packet);
|
||||
|
||||
if (packet.Type != PacketType.AgentUpdate)
|
||||
m_PacketsReceived++;
|
||||
|
||||
// Check for duplicate packets.. packets that the client is
|
||||
// resending because it didn't receive our ack
|
||||
//
|
||||
lock (m_DupeTracker)
|
||||
{
|
||||
if (m_DupeTracker.ContainsKey(packet.Header.Sequence))
|
||||
return;
|
||||
|
||||
m_DupeTracker.Add(packet.Header.Sequence,
|
||||
Util.UnixTimeSinceEpoch());
|
||||
}
|
||||
|
||||
m_Client.ProcessInPacket(packet);
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
m_PacketQueue.Flush();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
m_NeedAck.Clear();
|
||||
m_PendingAcks.Clear();
|
||||
m_Sequence += 1000000;
|
||||
}
|
||||
|
||||
private void ProcessAck(uint id)
|
||||
{
|
||||
AckData data;
|
||||
Packet packet;
|
||||
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
if (!m_NeedAck.TryGetValue(id, out data))
|
||||
return;
|
||||
|
||||
packet = data.Packet;
|
||||
|
||||
m_NeedAck.Remove(id);
|
||||
m_UnackedBytes -= packet.ToBytes().Length;
|
||||
|
||||
m_LastAck = System.Environment.TickCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate packet sequence numbers in a threadsave manner
|
||||
//
|
||||
protected uint NextPacketSequenceNumber()
|
||||
{
|
||||
// Set the sequence number
|
||||
uint seq = 1;
|
||||
lock (m_SequenceLock)
|
||||
{
|
||||
if (m_Sequence >= MAX_SEQUENCE)
|
||||
{
|
||||
m_Sequence = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Sequence++;
|
||||
}
|
||||
seq = m_Sequence;
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
public ClientInfo GetClientInfo()
|
||||
{
|
||||
ClientInfo info = new ClientInfo();
|
||||
info.pendingAcks = m_PendingAcks;
|
||||
info.needAck = new Dictionary<uint, byte[]>();
|
||||
|
||||
lock (m_NeedAck)
|
||||
{
|
||||
foreach (uint key in m_NeedAck.Keys)
|
||||
info.needAck.Add(key, m_NeedAck[key].Packet.ToBytes());
|
||||
}
|
||||
|
||||
LLQueItem[] queitems = m_PacketQueue.GetQueueArray();
|
||||
|
||||
for (int i = 0; i < queitems.Length; i++)
|
||||
{
|
||||
if (queitems[i].Incoming == false)
|
||||
info.out_packets.Add(queitems[i].Packet.ToBytes());
|
||||
}
|
||||
|
||||
info.sequence = m_Sequence;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void SetClientInfo(ClientInfo info)
|
||||
{
|
||||
m_PendingAcks = info.pendingAcks;
|
||||
m_NeedAck = new Dictionary<uint, AckData>();
|
||||
|
||||
Packet packet = null;
|
||||
int packetEnd = 0;
|
||||
byte[] zero = new byte[3000];
|
||||
|
||||
foreach (uint key in info.needAck.Keys)
|
||||
{
|
||||
byte[] buff = info.needAck[key];
|
||||
packetEnd = buff.Length - 1;
|
||||
|
||||
try
|
||||
{
|
||||
packet = PacketPool.Instance.GetPacket(buff, ref packetEnd, zero);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
m_NeedAck.Add(key, new AckData(packet, null));
|
||||
}
|
||||
|
||||
m_Sequence = info.sequence;
|
||||
}
|
||||
|
||||
public void AddImportantPacket(PacketType type)
|
||||
{
|
||||
if (m_ImportantPackets.Contains(type))
|
||||
return;
|
||||
|
||||
m_ImportantPackets.Add(type);
|
||||
}
|
||||
|
||||
public void RemoveImportantPacket(PacketType type)
|
||||
{
|
||||
if (!m_ImportantPackets.Contains(type))
|
||||
return;
|
||||
|
||||
m_ImportantPackets.Remove(type);
|
||||
}
|
||||
|
||||
private void DropResend(Object id)
|
||||
{
|
||||
foreach (AckData data in new List<AckData>(m_NeedAck.Values))
|
||||
{
|
||||
if (data.Identifier != null && data.Identifier == id)
|
||||
{
|
||||
m_NeedAck.Remove(data.Packet.Header.Sequence);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerOnPacketDrop(Packet packet, Object id)
|
||||
{
|
||||
PacketDrop handlerPacketDrop = OnPacketDrop;
|
||||
|
||||
if (handlerPacketDrop == null)
|
||||
return;
|
||||
|
||||
handlerPacketDrop(packet, id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,567 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
using libsecondlife;
|
||||
using libsecondlife.Packets;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Statistics;
|
||||
using OpenSim.Framework.Statistics.Interfaces;
|
||||
using Timer=System.Timers.Timer;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public class LLPacketQueue : IPullStatsProvider
|
||||
{
|
||||
private static readonly log4net.ILog m_log
|
||||
= log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
private bool m_enabled = true;
|
||||
|
||||
private BlockingQueue<LLQueItem> SendQueue;
|
||||
|
||||
private Queue<LLQueItem> IncomingPacketQueue;
|
||||
private Queue<LLQueItem> OutgoingPacketQueue;
|
||||
private Queue<LLQueItem> ResendOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> LandOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> WindOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> CloudOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> TaskOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> TaskLowpriorityPacketQueue;
|
||||
private Queue<LLQueItem> TextureOutgoingPacketQueue;
|
||||
private Queue<LLQueItem> AssetOutgoingPacketQueue;
|
||||
|
||||
// private Dictionary<uint, uint> PendingAcks = new Dictionary<uint, uint>();
|
||||
// private Dictionary<uint, Packet> NeedAck = new Dictionary<uint, Packet>();
|
||||
|
||||
// All throttle times and number of bytes are calculated by dividing by this value
|
||||
// This value also determines how many times per throttletimems the timer will run
|
||||
// If throttleimems is 1000 ms, then the timer will fire every 1000/7 milliseconds
|
||||
|
||||
private int throttleTimeDivisor = 7;
|
||||
|
||||
private int throttletimems = 1000;
|
||||
|
||||
private LLPacketThrottle ResendThrottle;
|
||||
private LLPacketThrottle LandThrottle;
|
||||
private LLPacketThrottle WindThrottle;
|
||||
private LLPacketThrottle CloudThrottle;
|
||||
private LLPacketThrottle TaskThrottle;
|
||||
private LLPacketThrottle AssetThrottle;
|
||||
private LLPacketThrottle TextureThrottle;
|
||||
private LLPacketThrottle TotalThrottle;
|
||||
|
||||
// private long LastThrottle;
|
||||
// private long ThrottleInterval;
|
||||
private Timer throttleTimer;
|
||||
|
||||
private LLUUID m_agentId;
|
||||
|
||||
public LLPacketQueue(LLUUID agentId)
|
||||
{
|
||||
// While working on this, the BlockingQueue had me fooled for a bit.
|
||||
// The Blocking queue causes the thread to stop until there's something
|
||||
// in it to process. it's an on-purpose threadlock though because
|
||||
// without it, the clientloop will suck up all sim resources.
|
||||
|
||||
SendQueue = new BlockingQueue<LLQueItem>();
|
||||
|
||||
IncomingPacketQueue = new Queue<LLQueItem>();
|
||||
OutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
ResendOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
LandOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
WindOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
CloudOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
TaskOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
TaskLowpriorityPacketQueue = new Queue<LLQueItem>();
|
||||
TextureOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
AssetOutgoingPacketQueue = new Queue<LLQueItem>();
|
||||
|
||||
|
||||
// Set up the throttle classes (min, max, current) in bytes
|
||||
ResendThrottle = new LLPacketThrottle(5000, 100000, 16000);
|
||||
LandThrottle = new LLPacketThrottle(1000, 100000, 2000);
|
||||
WindThrottle = new LLPacketThrottle(0, 100000, 0);
|
||||
CloudThrottle = new LLPacketThrottle(0, 100000, 0);
|
||||
TaskThrottle = new LLPacketThrottle(1000, 800000, 3000);
|
||||
AssetThrottle = new LLPacketThrottle(1000, 800000, 1000);
|
||||
TextureThrottle = new LLPacketThrottle(1000, 800000, 4000);
|
||||
// Total Throttle trumps all
|
||||
// Number of bytes allowed to go out per second. (256kbps per client)
|
||||
TotalThrottle = new LLPacketThrottle(0, 1500000, 28000);
|
||||
|
||||
throttleTimer = new Timer((int) (throttletimems/throttleTimeDivisor));
|
||||
throttleTimer.Elapsed += new ElapsedEventHandler(ThrottleTimerElapsed);
|
||||
throttleTimer.Start();
|
||||
|
||||
// TIMERS needed for this
|
||||
// LastThrottle = DateTime.Now.Ticks;
|
||||
// ThrottleInterval = (long)(throttletimems/throttleTimeDivisor);
|
||||
|
||||
m_agentId = agentId;
|
||||
|
||||
if (StatsManager.SimExtraStats != null)
|
||||
{
|
||||
StatsManager.SimExtraStats.RegisterPacketQueueStatsProvider(m_agentId, this);
|
||||
}
|
||||
}
|
||||
|
||||
/* STANDARD QUEUE MANIPULATION INTERFACES */
|
||||
|
||||
|
||||
public void Enqueue(LLQueItem item)
|
||||
{
|
||||
if (!m_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// We could micro lock, but that will tend to actually
|
||||
// probably be worse than just synchronizing on SendQueue
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
SendQueue.Enqueue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Incoming)
|
||||
{
|
||||
SendQueue.PriorityEnqueue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (this)
|
||||
{
|
||||
switch (item.throttleType)
|
||||
{
|
||||
case ThrottleOutPacketType.Resend:
|
||||
ThrottleCheck(ref ResendThrottle, ref ResendOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Texture:
|
||||
ThrottleCheck(ref TextureThrottle, ref TextureOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Task:
|
||||
ThrottleCheck(ref TaskThrottle, ref TaskOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.LowpriorityTask:
|
||||
ThrottleCheck(ref TaskThrottle, ref TaskLowpriorityPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Land:
|
||||
ThrottleCheck(ref LandThrottle, ref LandOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Asset:
|
||||
ThrottleCheck(ref AssetThrottle, ref AssetOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Cloud:
|
||||
ThrottleCheck(ref CloudThrottle, ref CloudOutgoingPacketQueue, item);
|
||||
break;
|
||||
case ThrottleOutPacketType.Wind:
|
||||
ThrottleCheck(ref WindThrottle, ref WindOutgoingPacketQueue, item);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Acknowledgements and other such stuff should go directly to the blocking Queue
|
||||
// Throttling them may and likely 'will' be problematic
|
||||
SendQueue.PriorityEnqueue(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LLQueItem Dequeue()
|
||||
{
|
||||
return SendQueue.Dequeue();
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
while (PacketsWaiting())
|
||||
{
|
||||
//Now comes the fun part.. we dump all our elements into m_packetQueue that we've saved up.
|
||||
if (ResendOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(ResendOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (LandOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(LandOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (WindOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(WindOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (CloudOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(CloudOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (TaskOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.PriorityEnqueue(TaskOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (TaskLowpriorityPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(TaskLowpriorityPacketQueue.Dequeue());
|
||||
}
|
||||
if (TextureOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(TextureOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
if (AssetOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
SendQueue.Enqueue(AssetOutgoingPacketQueue.Dequeue());
|
||||
}
|
||||
}
|
||||
// m_log.Info("[THROTTLE]: Processed " + throttleLoops + " packets");
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Flush();
|
||||
|
||||
m_enabled = false;
|
||||
throttleTimer.Stop();
|
||||
|
||||
if (StatsManager.SimExtraStats != null)
|
||||
{
|
||||
StatsManager.SimExtraStats.DeregisterPacketQueueStatsProvider(m_agentId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetCounters()
|
||||
{
|
||||
ResendThrottle.Reset();
|
||||
LandThrottle.Reset();
|
||||
WindThrottle.Reset();
|
||||
CloudThrottle.Reset();
|
||||
TaskThrottle.Reset();
|
||||
AssetThrottle.Reset();
|
||||
TextureThrottle.Reset();
|
||||
TotalThrottle.Reset();
|
||||
}
|
||||
|
||||
private bool PacketsWaiting()
|
||||
{
|
||||
return (ResendOutgoingPacketQueue.Count > 0 ||
|
||||
LandOutgoingPacketQueue.Count > 0 ||
|
||||
WindOutgoingPacketQueue.Count > 0 ||
|
||||
CloudOutgoingPacketQueue.Count > 0 ||
|
||||
TaskOutgoingPacketQueue.Count > 0 ||
|
||||
TaskLowpriorityPacketQueue.Count > 0 ||
|
||||
AssetOutgoingPacketQueue.Count > 0 ||
|
||||
TextureOutgoingPacketQueue.Count > 0);
|
||||
}
|
||||
|
||||
public void ProcessThrottle()
|
||||
{
|
||||
// I was considering this.. Will an event fire if the thread it's on is blocked?
|
||||
|
||||
// Then I figured out.. it doesn't really matter.. because this thread won't be blocked for long
|
||||
// The General overhead of the UDP protocol gets sent to the queue un-throttled by this
|
||||
// so This'll pick up about around the right time.
|
||||
|
||||
int MaxThrottleLoops = 4550; // 50*7 packets can be dequeued at once.
|
||||
int throttleLoops = 0;
|
||||
|
||||
// We're going to dequeue all of the saved up packets until
|
||||
// we've hit the throttle limit or there's no more packets to send
|
||||
lock (this)
|
||||
{
|
||||
ResetCounters();
|
||||
// m_log.Info("[THROTTLE]: Entering Throttle");
|
||||
while (TotalThrottle.UnderLimit() && PacketsWaiting() &&
|
||||
(throttleLoops <= MaxThrottleLoops))
|
||||
{
|
||||
throttleLoops++;
|
||||
//Now comes the fun part.. we dump all our elements into m_packetQueue that we've saved up.
|
||||
if (ResendThrottle.UnderLimit() && ResendOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = ResendOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
ResendThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (LandThrottle.UnderLimit() && LandOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = LandOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
LandThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (WindThrottle.UnderLimit() && WindOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = WindOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
WindThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (CloudThrottle.UnderLimit() && CloudOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = CloudOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
CloudThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (TaskThrottle.UnderLimit() && (TaskOutgoingPacketQueue.Count > 0 || TaskLowpriorityPacketQueue.Count > 0))
|
||||
{
|
||||
LLQueItem qpack;
|
||||
if (TaskOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
qpack = TaskOutgoingPacketQueue.Dequeue();
|
||||
SendQueue.PriorityEnqueue(qpack);
|
||||
}
|
||||
else
|
||||
{
|
||||
qpack = TaskLowpriorityPacketQueue.Dequeue();
|
||||
SendQueue.Enqueue(qpack);
|
||||
}
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
TaskThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (TextureThrottle.UnderLimit() && TextureOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = TextureOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
TextureThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
if (AssetThrottle.UnderLimit() && AssetOutgoingPacketQueue.Count > 0)
|
||||
{
|
||||
LLQueItem qpack = AssetOutgoingPacketQueue.Dequeue();
|
||||
|
||||
SendQueue.Enqueue(qpack);
|
||||
TotalThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
AssetThrottle.Add(qpack.Packet.ToBytes().Length);
|
||||
}
|
||||
}
|
||||
// m_log.Info("[THROTTLE]: Processed " + throttleLoops + " packets");
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrottleTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
// just to change the signature, and that ProcessThrottle
|
||||
// will be used elsewhere possibly
|
||||
ProcessThrottle();
|
||||
}
|
||||
|
||||
private void ThrottleCheck(ref LLPacketThrottle throttle, ref Queue<LLQueItem> q, LLQueItem item)
|
||||
{
|
||||
// The idea.. is if the packet throttle queues are empty
|
||||
// and the client is under throttle for the type. Queue
|
||||
// it up directly. This basically short cuts having to
|
||||
// wait for the timer to fire to put things into the
|
||||
// output queue
|
||||
|
||||
if ((q.Count == 0) && (throttle.UnderLimit()))
|
||||
{
|
||||
Monitor.Enter(this);
|
||||
throttle.Add(item.Packet.ToBytes().Length);
|
||||
TotalThrottle.Add(item.Packet.ToBytes().Length);
|
||||
SendQueue.Enqueue(item);
|
||||
Monitor.Pulse(this);
|
||||
Monitor.Exit(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
q.Enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static int ScaleThrottle(int value, int curmax, int newmax)
|
||||
{
|
||||
return (value / curmax) * newmax;
|
||||
}
|
||||
|
||||
public byte[] GetThrottlesPacked(float multiplier)
|
||||
{
|
||||
int singlefloat = 4;
|
||||
float tResend = ResendThrottle.Throttle*multiplier;
|
||||
float tLand = LandThrottle.Throttle*multiplier;
|
||||
float tWind = WindThrottle.Throttle*multiplier;
|
||||
float tCloud = CloudThrottle.Throttle*multiplier;
|
||||
float tTask = TaskThrottle.Throttle*multiplier;
|
||||
float tTexture = TextureThrottle.Throttle*multiplier;
|
||||
float tAsset = AssetThrottle.Throttle*multiplier;
|
||||
|
||||
byte[] throttles = new byte[singlefloat*7];
|
||||
int i = 0;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tResend), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tLand), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tWind), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tCloud), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tTask), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tTexture), 0, throttles, singlefloat*i, singlefloat);
|
||||
i++;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(tAsset), 0, throttles, singlefloat*i, singlefloat);
|
||||
|
||||
return throttles;
|
||||
}
|
||||
|
||||
public void SetThrottleFromClient(byte[] throttle)
|
||||
{
|
||||
// From mantis http://opensimulator.org/mantis/view.php?id=1374
|
||||
// it appears that sometimes we are receiving empty throttle byte arrays.
|
||||
// TODO: Investigate this behaviour
|
||||
if (throttle.Length == 0)
|
||||
{
|
||||
m_log.Warn("[PACKET QUEUE]: SetThrottleFromClient unexpectedly received a throttle byte array containing no elements!");
|
||||
return;
|
||||
}
|
||||
|
||||
int tResend = -1;
|
||||
int tLand = -1;
|
||||
int tWind = -1;
|
||||
int tCloud = -1;
|
||||
int tTask = -1;
|
||||
int tTexture = -1;
|
||||
int tAsset = -1;
|
||||
int tall = -1;
|
||||
int singlefloat = 4;
|
||||
|
||||
//Agent Throttle Block contains 7 single floatingpoint values.
|
||||
int j = 0;
|
||||
|
||||
// Some Systems may be big endian...
|
||||
// it might be smart to do this check more often...
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
for (int i = 0; i < 7; i++)
|
||||
Array.Reverse(throttle, j + i*singlefloat, singlefloat);
|
||||
|
||||
// values gotten from libsecondlife.org/wiki/Throttle. Thanks MW_
|
||||
// bytes
|
||||
// Convert to integer, since.. the full fp space isn't used.
|
||||
tResend = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tLand = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tWind = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tCloud = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tTask = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tTexture = (int) BitConverter.ToSingle(throttle, j);
|
||||
j += singlefloat;
|
||||
tAsset = (int) BitConverter.ToSingle(throttle, j);
|
||||
|
||||
tall = tResend + tLand + tWind + tCloud + tTask + tTexture + tAsset;
|
||||
/*
|
||||
m_log.Info("[CLIENT]: Client AgentThrottle - Got throttle:resendbytes=" + tResend +
|
||||
" landbytes=" + tLand +
|
||||
" windbytes=" + tWind +
|
||||
" cloudbytes=" + tCloud +
|
||||
" taskbytes=" + tTask +
|
||||
" texturebytes=" + tTexture +
|
||||
" Assetbytes=" + tAsset +
|
||||
" Allbytes=" + tall);
|
||||
*/
|
||||
|
||||
// Total Sanity
|
||||
// Make sure that the client sent sane total values.
|
||||
|
||||
// If the client didn't send acceptable values....
|
||||
// Scale the clients values down until they are acceptable.
|
||||
|
||||
if (tall <= TotalThrottle.Max)
|
||||
{
|
||||
ResendThrottle.Throttle = tResend;
|
||||
LandThrottle.Throttle = tLand;
|
||||
WindThrottle.Throttle = tWind;
|
||||
CloudThrottle.Throttle = tCloud;
|
||||
TaskThrottle.Throttle = tTask;
|
||||
TextureThrottle.Throttle = tTexture;
|
||||
AssetThrottle.Throttle = tAsset;
|
||||
TotalThrottle.Throttle = tall;
|
||||
}
|
||||
// else if (tall < 1)
|
||||
// {
|
||||
// // client is stupid, penalize him by minning everything
|
||||
// ResendThrottle.Throttle = ResendThrottle.Min;
|
||||
// LandThrottle.Throttle = LandThrottle.Min;
|
||||
// WindThrottle.Throttle = WindThrottle.Min;
|
||||
// CloudThrottle.Throttle = CloudThrottle.Min;
|
||||
// TaskThrottle.Throttle = TaskThrottle.Min;
|
||||
// TextureThrottle.Throttle = TextureThrottle.Min;
|
||||
// AssetThrottle.Throttle = AssetThrottle.Min;
|
||||
// TotalThrottle.Throttle = TotalThrottle.Min;
|
||||
// }
|
||||
else
|
||||
{
|
||||
// we're over so figure out percentages and use those
|
||||
ResendThrottle.Throttle = tResend;
|
||||
|
||||
LandThrottle.Throttle = ScaleThrottle(tLand, tall, TotalThrottle.Max);
|
||||
WindThrottle.Throttle = ScaleThrottle(tWind, tall, TotalThrottle.Max);
|
||||
CloudThrottle.Throttle = ScaleThrottle(tCloud, tall, TotalThrottle.Max);
|
||||
TaskThrottle.Throttle = ScaleThrottle(tTask, tall, TotalThrottle.Max);
|
||||
TextureThrottle.Throttle = ScaleThrottle(tTexture, tall, TotalThrottle.Max);
|
||||
AssetThrottle.Throttle = ScaleThrottle(tAsset, tall, TotalThrottle.Max);
|
||||
TotalThrottle.Throttle = TotalThrottle.Max;
|
||||
}
|
||||
// effectively wiggling the slider causes things reset
|
||||
// ResetCounters(); // DO NOT reset, better to send less for one period than more
|
||||
}
|
||||
|
||||
// See IPullStatsProvider
|
||||
public string GetStats()
|
||||
{
|
||||
return string.Format("{0,7} {1,7} {2,7} {3,7} {4,7} {5,7} {6,7} {7,7} {8,7} {9,7}",
|
||||
SendQueue.Count(),
|
||||
IncomingPacketQueue.Count,
|
||||
OutgoingPacketQueue.Count,
|
||||
ResendOutgoingPacketQueue.Count,
|
||||
LandOutgoingPacketQueue.Count,
|
||||
WindOutgoingPacketQueue.Count,
|
||||
CloudOutgoingPacketQueue.Count,
|
||||
TaskOutgoingPacketQueue.Count,
|
||||
TextureOutgoingPacketQueue.Count,
|
||||
AssetOutgoingPacketQueue.Count);
|
||||
}
|
||||
|
||||
public LLQueItem[] GetQueueArray()
|
||||
{
|
||||
return SendQueue.GetQueueArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using libsecondlife;
|
||||
using libsecondlife.Packets;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Communications.Cache;
|
||||
using OpenSim.Region.ClientStack.LindenUDP;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public class LLPacketServer
|
||||
{
|
||||
//private static readonly log4net.ILog m_log
|
||||
// = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
private readonly LLClientStackNetworkHandler m_networkHandler;
|
||||
private IScene m_scene;
|
||||
|
||||
//private readonly ClientManager m_clientManager = new ClientManager();
|
||||
//public ClientManager ClientManager
|
||||
//{
|
||||
// get { return m_clientManager; }
|
||||
//}
|
||||
|
||||
public LLPacketServer(LLClientStackNetworkHandler networkHandler)
|
||||
{
|
||||
m_networkHandler = networkHandler;
|
||||
m_networkHandler.RegisterPacketServer(this);
|
||||
}
|
||||
|
||||
public IScene LocalScene
|
||||
{
|
||||
set { m_scene = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="circuitCode"></param>
|
||||
/// <param name="packet"></param>
|
||||
public virtual void InPacket(uint circuitCode, Packet packet)
|
||||
{
|
||||
m_scene.ClientManager.InPacket(circuitCode, packet);
|
||||
}
|
||||
|
||||
protected virtual IClientAPI CreateNewClient(EndPoint remoteEP, UseCircuitCodePacket initialcirpack,
|
||||
ClientManager clientManager, IScene scene, AssetCache assetCache,
|
||||
LLPacketServer packServer, AgentCircuitManager authenSessions,
|
||||
LLUUID agentId, LLUUID sessionId, uint circuitCode, EndPoint proxyEP)
|
||||
{
|
||||
return
|
||||
new LLClientView(remoteEP, scene, assetCache, packServer, authenSessions, agentId, sessionId, circuitCode, proxyEP);
|
||||
}
|
||||
|
||||
public virtual bool AddNewClient(EndPoint epSender, UseCircuitCodePacket useCircuit, AssetCache assetCache,
|
||||
AgentCircuitManager authenticateSessionsClass, EndPoint proxyEP)
|
||||
{
|
||||
IClientAPI newuser;
|
||||
|
||||
if (m_scene.ClientManager.TryGetClient(useCircuit.CircuitCode.Code, out newuser))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
newuser = CreateNewClient(epSender, useCircuit, m_scene.ClientManager, m_scene, assetCache, this,
|
||||
authenticateSessionsClass, useCircuit.CircuitCode.ID,
|
||||
useCircuit.CircuitCode.SessionID, useCircuit.CircuitCode.Code, proxyEP);
|
||||
|
||||
m_scene.ClientManager.Add(useCircuit.CircuitCode.Code, newuser);
|
||||
|
||||
newuser.OnViewerEffect += m_scene.ClientManager.ViewerEffectHandler;
|
||||
newuser.OnLogout += LogoutHandler;
|
||||
newuser.OnConnectionClosed += CloseClient;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void LogoutHandler(IClientAPI client)
|
||||
{
|
||||
client.SendLogoutPacket();
|
||||
|
||||
CloseClient(client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="buffer"></param>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="flags"></param>
|
||||
/// <param name="circuitcode"></param>
|
||||
public virtual void SendPacketTo(byte[] buffer, int size, SocketFlags flags, uint circuitcode)
|
||||
{
|
||||
m_networkHandler.SendPacketTo(buffer, size, flags, circuitcode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="circuitcode"></param>
|
||||
public virtual void CloseCircuit(uint circuitcode)
|
||||
{
|
||||
m_networkHandler.RemoveClientCircuit(circuitcode);
|
||||
|
||||
//m_scene.ClientManager.CloseAllAgents(circuitcode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completely close down the given client.
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public virtual void CloseClient(IClientAPI client)
|
||||
{
|
||||
//m_log.Info("PacketServer:CloseClient()");
|
||||
|
||||
CloseCircuit(client.CircuitCode);
|
||||
m_scene.ClientManager.Remove(client.CircuitCode);
|
||||
client.Close(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public class LLPacketThrottle
|
||||
{
|
||||
private readonly int m_maxAllowableThrottle;
|
||||
private readonly int m_minAllowableThrottle;
|
||||
private int m_currentThrottle;
|
||||
private const int m_throttleTimeDivisor = 7;
|
||||
private int m_currentBytesSent;
|
||||
|
||||
public LLPacketThrottle(int Min, int Max, int Throttle)
|
||||
{
|
||||
m_maxAllowableThrottle = Max;
|
||||
m_minAllowableThrottle = Min;
|
||||
m_currentThrottle = Throttle;
|
||||
m_currentBytesSent = 0;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
m_currentBytesSent = 0;
|
||||
}
|
||||
|
||||
public bool UnderLimit()
|
||||
{
|
||||
return (m_currentBytesSent < (m_currentThrottle/m_throttleTimeDivisor));
|
||||
}
|
||||
|
||||
public int Add(int bytes)
|
||||
{
|
||||
m_currentBytesSent += bytes;
|
||||
return m_currentBytesSent;
|
||||
}
|
||||
|
||||
// Properties
|
||||
public int Max
|
||||
{
|
||||
get { return m_maxAllowableThrottle; }
|
||||
}
|
||||
|
||||
public int Min
|
||||
{
|
||||
get { return m_minAllowableThrottle; }
|
||||
}
|
||||
|
||||
public int Throttle
|
||||
{
|
||||
get { return m_currentThrottle; }
|
||||
set
|
||||
{
|
||||
if (value > m_maxAllowableThrottle)
|
||||
{
|
||||
m_currentThrottle = m_maxAllowableThrottle;
|
||||
}
|
||||
else if (value < m_minAllowableThrottle)
|
||||
{
|
||||
m_currentThrottle = m_minAllowableThrottle;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_currentThrottle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using libsecondlife.Packets;
|
||||
using OpenSim.Framework;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public class LLQueItem
|
||||
{
|
||||
public LLQueItem()
|
||||
{
|
||||
}
|
||||
|
||||
public Packet Packet;
|
||||
public bool Incoming;
|
||||
public ThrottleOutPacketType throttleType;
|
||||
public Object Identifier;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,545 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, http://opensimulator.org/
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSim Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using libsecondlife.Packets;
|
||||
using log4net;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Communications.Cache;
|
||||
using OpenSim.Region.ClientStack.LindenUDP;
|
||||
using OpenSim.Region.Environment.Scenes;
|
||||
|
||||
namespace OpenSim.Region.ClientStack.LindenUDP
|
||||
{
|
||||
public class LLUDPServer : LLClientStackNetworkHandler, IClientNetworkServer
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
protected Dictionary<EndPoint, uint> clientCircuits = new Dictionary<EndPoint, uint>();
|
||||
|
||||
//public Dictionary<uint, EndPoint> clientCircuits_reverse = new Dictionary<uint, EndPoint>();
|
||||
public Hashtable clientCircuits_reverse = Hashtable.Synchronized(new Hashtable());
|
||||
|
||||
protected Dictionary<uint, EndPoint> proxyCircuits = new Dictionary<uint, EndPoint>();
|
||||
private Socket m_socket;
|
||||
protected IPEndPoint ServerIncoming;
|
||||
protected byte[] RecvBuffer = new byte[4096];
|
||||
protected byte[] ZeroBuffer = new byte[8192];
|
||||
protected IPEndPoint ipeSender;
|
||||
protected EndPoint epSender;
|
||||
protected EndPoint epProxy;
|
||||
protected int proxyPortOffset;
|
||||
protected AsyncCallback ReceivedData;
|
||||
protected LLPacketServer m_packetServer;
|
||||
protected Location m_location;
|
||||
|
||||
protected uint listenPort;
|
||||
protected bool Allow_Alternate_Port;
|
||||
protected IPAddress listenIP = IPAddress.Parse("0.0.0.0");
|
||||
protected IScene m_localScene;
|
||||
protected AssetCache m_assetCache;
|
||||
protected AgentCircuitManager m_authenticateSessionsClass;
|
||||
|
||||
public LLPacketServer PacketServer
|
||||
{
|
||||
get { return m_packetServer; }
|
||||
set { m_packetServer = value; }
|
||||
}
|
||||
|
||||
public IScene LocalScene
|
||||
{
|
||||
set
|
||||
{
|
||||
m_localScene = value;
|
||||
m_packetServer.LocalScene = m_localScene;
|
||||
m_location = new Location(m_localScene.RegionInfo.RegionHandle);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong RegionHandle
|
||||
{
|
||||
get { return m_location.RegionHandle; }
|
||||
}
|
||||
|
||||
Socket IClientNetworkServer.Server
|
||||
{
|
||||
get { return m_socket; }
|
||||
}
|
||||
|
||||
public bool HandlesRegion(Location x)
|
||||
{
|
||||
return x == m_location;
|
||||
}
|
||||
|
||||
public void AddScene(Scene x)
|
||||
{
|
||||
LocalScene = x;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
ServerListener();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
m_socket.Close();
|
||||
}
|
||||
|
||||
public LLUDPServer()
|
||||
{
|
||||
}
|
||||
|
||||
public LLUDPServer(IPAddress _listenIP, ref uint port, int proxyPortOffset, bool allow_alternate_port, AssetCache assetCache, AgentCircuitManager authenticateClass)
|
||||
{
|
||||
Initialise(_listenIP, ref port, proxyPortOffset, allow_alternate_port, assetCache, authenticateClass);
|
||||
}
|
||||
|
||||
public void Initialise(IPAddress _listenIP, ref uint port, int proxyPortOffset, bool allow_alternate_port, AssetCache assetCache, AgentCircuitManager authenticateClass)
|
||||
{
|
||||
this.proxyPortOffset = proxyPortOffset;
|
||||
listenPort = (uint) (port + proxyPortOffset);
|
||||
listenIP = _listenIP;
|
||||
Allow_Alternate_Port = allow_alternate_port;
|
||||
m_assetCache = assetCache;
|
||||
m_authenticateSessionsClass = authenticateClass;
|
||||
CreatePacketServer();
|
||||
|
||||
// Return new port
|
||||
// This because in Grid mode it is not really important what port the region listens to as long as it is correctly registered.
|
||||
// So the option allow_alternate_ports="true" was added to default.xml
|
||||
port = (uint)(listenPort - proxyPortOffset);
|
||||
}
|
||||
|
||||
protected virtual void CreatePacketServer()
|
||||
{
|
||||
new LLPacketServer(this);
|
||||
}
|
||||
|
||||
protected virtual void OnReceivedData(IAsyncResult result)
|
||||
{
|
||||
ipeSender = new IPEndPoint(listenIP, 0);
|
||||
epSender = (EndPoint) ipeSender;
|
||||
Packet packet = null;
|
||||
|
||||
int numBytes = 1;
|
||||
|
||||
try
|
||||
{
|
||||
numBytes = m_socket.EndReceiveFrom(result, ref epSender);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// TODO : Actually only handle those states that we have control over, re-throw everything else,
|
||||
// TODO: implement cases as we encounter them.
|
||||
//m_log.Error("[UDPSERVER]: Connection Error! - " + e.ToString());
|
||||
switch (e.SocketErrorCode)
|
||||
{
|
||||
case SocketError.AlreadyInProgress:
|
||||
case SocketError.NetworkReset:
|
||||
case SocketError.ConnectionReset:
|
||||
try
|
||||
{
|
||||
CloseEndPoint(epSender);
|
||||
}
|
||||
catch (Exception a)
|
||||
{
|
||||
m_log.Info("[UDPSERVER]: " + a.ToString());
|
||||
}
|
||||
try
|
||||
{
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender,
|
||||
ReceivedData, null);
|
||||
|
||||
// Ter: For some stupid reason ConnectionReset basically kills our async event structure..
|
||||
// so therefore.. we've got to tell the server to BeginReceiveFrom again.
|
||||
// This will happen over and over until we've gone through all packets
|
||||
// sent to and from this particular user.
|
||||
// Stupid I know..
|
||||
// but Flusing the buffer would be even more stupid... so, we're stuck with this ugly method.
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
}
|
||||
break;
|
||||
default:
|
||||
try
|
||||
{
|
||||
CloseEndPoint(epSender);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//m_log.Info("[UDPSERVER]" + a.ToString());
|
||||
}
|
||||
try
|
||||
{
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender,
|
||||
ReceivedData, null);
|
||||
|
||||
// Ter: For some stupid reason ConnectionReset basically kills our async event structure..
|
||||
// so therefore.. we've got to tell the server to BeginReceiveFrom again.
|
||||
// This will happen over and over until we've gone through all packets
|
||||
// sent to and from this particular user.
|
||||
// Stupid I know..
|
||||
// but Flusing the buffer would be even more stupid... so, we're stuck with this ugly method.
|
||||
}
|
||||
catch (SocketException e2)
|
||||
{
|
||||
m_log.Error("[UDPSERVER]: " + e2.ToString());
|
||||
}
|
||||
|
||||
// Here's some reference code! :D
|
||||
// Shutdown and restart the UDP listener! hehe
|
||||
// Shiny
|
||||
|
||||
//Server.Shutdown(SocketShutdown.Both);
|
||||
//CloseEndPoint(epSender);
|
||||
//ServerListener();
|
||||
break;
|
||||
}
|
||||
|
||||
//return;
|
||||
}
|
||||
catch (ObjectDisposedException e)
|
||||
{
|
||||
m_log.Debug("[UDPSERVER]: " + e.ToString());
|
||||
try
|
||||
{
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender,
|
||||
ReceivedData, null);
|
||||
|
||||
// Ter: For some stupid reason ConnectionReset basically kills our async event structure..
|
||||
// so therefore.. we've got to tell the server to BeginReceiveFrom again.
|
||||
// This will happen over and over until we've gone through all packets
|
||||
// sent to and from this particular user.
|
||||
// Stupid I know..
|
||||
// but Flusing the buffer would be even more stupid... so, we're stuck with this ugly method.
|
||||
}
|
||||
|
||||
catch (SocketException e2)
|
||||
{
|
||||
m_log.Error("[UDPSERVER]: " + e2.ToString());
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
//return;
|
||||
}
|
||||
|
||||
//System.Console.WriteLine("UDPServer : recieved message from {0}", epSender.ToString());
|
||||
epProxy = epSender;
|
||||
if (proxyPortOffset != 0)
|
||||
{
|
||||
epSender = PacketPool.DecodeProxyMessage(RecvBuffer, ref numBytes);
|
||||
}
|
||||
|
||||
int packetEnd = numBytes - 1;
|
||||
|
||||
try
|
||||
{
|
||||
packet = PacketPool.Instance.GetPacket(RecvBuffer, ref packetEnd, ZeroBuffer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.Debug("[UDPSERVER]: " + e.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender, ReceivedData, null);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
try
|
||||
{
|
||||
CloseEndPoint(epSender);
|
||||
}
|
||||
catch (Exception a)
|
||||
{
|
||||
m_log.Info("[UDPSERVER]: " + a.ToString());
|
||||
}
|
||||
try
|
||||
{
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender,
|
||||
ReceivedData, null);
|
||||
|
||||
// Ter: For some stupid reason ConnectionReset basically kills our async event structure..
|
||||
// so therefore.. we've got to tell the server to BeginReceiveFrom again.
|
||||
// This will happen over and over until we've gone through all packets
|
||||
// sent to and from this particular user.
|
||||
// Stupid I know..
|
||||
// but Flusing the buffer would be even more stupid... so, we're stuck with this ugly method.
|
||||
}
|
||||
catch (SocketException e5)
|
||||
{
|
||||
m_log.Error("[UDPSERVER]: " + e5.ToString());
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
|
||||
if (packet != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// do we already have a circuit for this endpoint
|
||||
uint circuit;
|
||||
|
||||
bool ret = false;
|
||||
lock (clientCircuits)
|
||||
{
|
||||
ret = clientCircuits.TryGetValue(epSender, out circuit);
|
||||
}
|
||||
if (ret)
|
||||
{
|
||||
//if so then send packet to the packetserver
|
||||
//m_log.Warn("[UDPSERVER]: ALREADY HAVE Circuit!");
|
||||
m_packetServer.InPacket(circuit, packet);
|
||||
}
|
||||
else if (packet.Type == PacketType.UseCircuitCode)
|
||||
{
|
||||
// new client
|
||||
m_log.Debug("[UDPSERVER]: Adding New Client");
|
||||
AddNewClient(packet);
|
||||
|
||||
UseCircuitCodePacket p = (UseCircuitCodePacket)packet;
|
||||
|
||||
// Ack the first UseCircuitCode packet
|
||||
PacketAckPacket ack_it = (PacketAckPacket)PacketPool.Instance.GetPacket(PacketType.PacketAck);
|
||||
// TODO: don't create new blocks if recycling an old packet
|
||||
ack_it.Packets = new PacketAckPacket.PacketsBlock[1];
|
||||
ack_it.Packets[0] = new PacketAckPacket.PacketsBlock();
|
||||
ack_it.Packets[0].ID = packet.Header.Sequence;
|
||||
ack_it.Header.Reliable = false;
|
||||
SendPacketTo(ack_it.ToBytes(),ack_it.ToBytes().Length,SocketFlags.None,p.CircuitCode.Code);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
m_log.Error("[UDPSERVER]: Exception in processing packet.");
|
||||
m_log.Debug("[UDPSERVER]: Adding New Client");
|
||||
try
|
||||
{
|
||||
AddNewClient(packet);
|
||||
}
|
||||
catch (Exception e3)
|
||||
{
|
||||
m_log.Error("[UDPSERVER]: Adding New Client threw exception " + e3.ToString());
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender,
|
||||
ReceivedData, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void CloseEndPoint(EndPoint sender)
|
||||
{
|
||||
uint circuit;
|
||||
lock (clientCircuits)
|
||||
{
|
||||
if (clientCircuits.TryGetValue(sender, out circuit))
|
||||
{
|
||||
m_packetServer.CloseCircuit(circuit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void AddNewClient(Packet packet)
|
||||
{
|
||||
//Slave regions don't accept new clients
|
||||
if (m_localScene.Region_Status != RegionStatus.SlaveScene)
|
||||
{
|
||||
if (!(packet is UseCircuitCodePacket))
|
||||
return;
|
||||
|
||||
UseCircuitCodePacket useCircuit = (UseCircuitCodePacket) packet;
|
||||
lock (clientCircuits)
|
||||
{
|
||||
if (!clientCircuits.ContainsKey(epSender))
|
||||
clientCircuits.Add(epSender, useCircuit.CircuitCode.Code);
|
||||
else
|
||||
m_log.Error("[UDPSERVER]: clientCircuits already contans entry for user " + useCircuit.CircuitCode.Code.ToString() + ". NOT adding.");
|
||||
}
|
||||
|
||||
// This doesn't need locking as it's synchronized data
|
||||
if (!clientCircuits_reverse.ContainsKey(useCircuit.CircuitCode.Code))
|
||||
clientCircuits_reverse.Add(useCircuit.CircuitCode.Code, epSender);
|
||||
else
|
||||
m_log.Error("[UDPSERVER]: clientCurcuits_reverse already contains entry for user " + useCircuit.CircuitCode.Code.ToString() + ". NOT adding.");
|
||||
|
||||
|
||||
lock (proxyCircuits)
|
||||
{
|
||||
if (!proxyCircuits.ContainsKey(useCircuit.CircuitCode.Code))
|
||||
proxyCircuits.Add(useCircuit.CircuitCode.Code, epProxy);
|
||||
else
|
||||
m_log.Error("[UDPSERVER]: proxyCircuits already contains entry for user " + useCircuit.CircuitCode.Code.ToString() + ". NOT adding.");
|
||||
}
|
||||
|
||||
PacketServer.AddNewClient(epSender, useCircuit, m_assetCache, m_authenticateSessionsClass, epProxy);
|
||||
}
|
||||
PacketPool.Instance.ReturnPacket(packet);
|
||||
}
|
||||
|
||||
public void ServerListener()
|
||||
{
|
||||
uint newPort = listenPort;
|
||||
m_log.Info("[SERVER]: Opening UDP socket on " + listenIP.ToString() + " " + newPort + ".");
|
||||
|
||||
ServerIncoming = new IPEndPoint(listenIP, (int)newPort);
|
||||
m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
m_socket.Bind(ServerIncoming);
|
||||
// Add flags to the UDP socket to prevent "Socket forcibly closed by host"
|
||||
// uint IOC_IN = 0x80000000;
|
||||
// uint IOC_VENDOR = 0x18000000;
|
||||
// uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
|
||||
// TODO: this apparently works in .NET but not in Mono, need to sort out the right flags here.
|
||||
// m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
|
||||
|
||||
listenPort = newPort;
|
||||
|
||||
m_log.Info("[SERVER]: UDP socket bound, getting ready to listen");
|
||||
|
||||
ipeSender = new IPEndPoint(listenIP, 0);
|
||||
epSender = (EndPoint)ipeSender;
|
||||
ReceivedData = new AsyncCallback(OnReceivedData);
|
||||
m_socket.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref epSender, ReceivedData, null);
|
||||
|
||||
m_log.Info("[SERVER]: Listening on port " + newPort);
|
||||
}
|
||||
|
||||
public virtual void RegisterPacketServer(LLPacketServer server)
|
||||
{
|
||||
m_packetServer = server;
|
||||
}
|
||||
|
||||
public virtual void SendPacketTo(byte[] buffer, int size, SocketFlags flags, uint circuitcode)
|
||||
//EndPoint packetSender)
|
||||
{
|
||||
// find the endpoint for this circuit
|
||||
EndPoint sendto = null;
|
||||
try {
|
||||
sendto = (EndPoint)clientCircuits_reverse[circuitcode];
|
||||
} catch {
|
||||
// Exceptions here mean there is no circuit
|
||||
m_log.Warn("Circuit not found, not sending packet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendto != null)
|
||||
{
|
||||
//we found the endpoint so send the packet to it
|
||||
if (proxyPortOffset != 0)
|
||||
{
|
||||
//MainLog.Instance.Verbose("UDPSERVER", "SendPacketTo proxy " + proxyCircuits[circuitcode].ToString() + ": client " + sendto.ToString());
|
||||
PacketPool.EncodeProxyMessage(buffer, ref size, sendto);
|
||||
m_socket.SendTo(buffer, size, flags, proxyCircuits[circuitcode]);
|
||||
}
|
||||
else
|
||||
{
|
||||
//MainLog.Instance.Verbose("UDPSERVER", "SendPacketTo : client " + sendto.ToString());
|
||||
m_socket.SendTo(buffer, size, flags, sendto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void RemoveClientCircuit(uint circuitcode)
|
||||
{
|
||||
EndPoint sendto = null;
|
||||
if (clientCircuits_reverse.Contains(circuitcode))
|
||||
{
|
||||
sendto = (EndPoint)clientCircuits_reverse[circuitcode];
|
||||
|
||||
clientCircuits_reverse.Remove(circuitcode);
|
||||
|
||||
lock (clientCircuits)
|
||||
{
|
||||
if (sendto != null)
|
||||
{
|
||||
clientCircuits.Remove(sendto);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.DebugFormat(
|
||||
"[UDPSERVER]: endpoint for circuit code {0} in RemoveClientCircuit() was unexpectedly null!", circuitcode);
|
||||
}
|
||||
}
|
||||
lock (proxyCircuits)
|
||||
{
|
||||
proxyCircuits.Remove(circuitcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RestoreClient(AgentCircuitData circuit, EndPoint userEP, EndPoint proxyEP)
|
||||
{
|
||||
//MainLog.Instance.Verbose("UDPSERVER", "RestoreClient");
|
||||
|
||||
UseCircuitCodePacket useCircuit = new UseCircuitCodePacket();
|
||||
useCircuit.CircuitCode.Code = circuit.circuitcode;
|
||||
useCircuit.CircuitCode.ID = circuit.AgentID;
|
||||
useCircuit.CircuitCode.SessionID = circuit.SessionID;
|
||||
|
||||
lock (clientCircuits)
|
||||
{
|
||||
if (!clientCircuits.ContainsKey(userEP))
|
||||
clientCircuits.Add(userEP, useCircuit.CircuitCode.Code);
|
||||
else
|
||||
m_log.Error("[UDPSERVER]: clientCircuits already contans entry for user " + useCircuit.CircuitCode.Code.ToString() + ". NOT adding.");
|
||||
}
|
||||
|
||||
// This data structure is synchronized, so we don't need the lock
|
||||
if (!clientCircuits_reverse.ContainsKey(useCircuit.CircuitCode.Code))
|
||||
clientCircuits_reverse.Add(useCircuit.CircuitCode.Code, userEP);
|
||||
else
|
||||
m_log.Error("[UDPSERVER]: clientCurcuits_reverse already contains entry for user " + useCircuit.CircuitCode.Code.ToString() + ". NOT adding.");
|
||||
|
||||
lock (proxyCircuits)
|
||||
{
|
||||
if (!proxyCircuits.ContainsKey(useCircuit.CircuitCode.Code))
|
||||
{
|
||||
proxyCircuits.Add(useCircuit.CircuitCode.Code, proxyEP);
|
||||
}
|
||||
else
|
||||
{
|
||||
// re-set proxy endpoint
|
||||
proxyCircuits.Remove(useCircuit.CircuitCode.Code);
|
||||
proxyCircuits.Add(useCircuit.CircuitCode.Code, proxyEP);
|
||||
}
|
||||
}
|
||||
|
||||
PacketServer.AddNewClient(userEP, useCircuit, m_assetCache, m_authenticateSessionsClass, proxyEP);
|
||||
}
|
||||
}
|
||||
}
|
36
prebuild.xml
36
prebuild.xml
|
@ -888,6 +888,42 @@
|
|||
</Files>
|
||||
</Project>
|
||||
|
||||
<Project name="OpenSim.Region.ClientStack.FunSLUDP" path="OpenSim/Region/ClientStack/FunSLUDP" type="Library">
|
||||
<Configuration name="Debug">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
</Options>
|
||||
</Configuration>
|
||||
<Configuration name="Release">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
</Options>
|
||||
</Configuration>
|
||||
|
||||
<ReferencePath>../../../../bin/</ReferencePath>
|
||||
<Reference name="System" localCopy="false"/>
|
||||
<Reference name="System.Xml"/>
|
||||
<Reference name="libsecondlife.dll"/>
|
||||
<Reference name="Axiom.MathLib.dll"/>
|
||||
<Reference name="OpenSim.Region.Environment"/>
|
||||
<Reference name="OpenSim.Framework"/>
|
||||
<Reference name="OpenSim.Data"/>
|
||||
<Reference name="OpenSim.Framework.Servers"/>
|
||||
<Reference name="OpenSim.Framework.Console"/>
|
||||
<Reference name="OpenSim.Framework.Communications"/>
|
||||
<Reference name="OpenSim.Framework.Statistics"/>
|
||||
<Reference name="OpenSim.Region.ClientStack"/>
|
||||
<Reference name="OpenSim.Region.Communications.Local"/>
|
||||
<Reference name="OpenSim.Region.Physics.Manager"/>
|
||||
<Reference name="XMLRPC.dll"/>
|
||||
<Reference name="Nini.dll" />
|
||||
<Reference name="log4net.dll"/>
|
||||
|
||||
<Files>
|
||||
<Match pattern="*.cs" recurse="true"/>
|
||||
</Files>
|
||||
</Project>
|
||||
|
||||
<!-- Datastore Plugins -->
|
||||
<Project name="OpenSim.Data.Null" path="OpenSim/Data/Null" type="Library">
|
||||
<Configuration name="Debug">
|
||||
|
|
Loading…
Reference in New Issue