/*
* 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 OpenSimulator 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.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Net.Sockets;
using System.Threading;
namespace OSHttpServer
{
///
/// Timeout Manager. Checks for dead clients. Clients with open connections that are not doing anything. Closes sessions opened with keepalive.
///
public static class ContextTimeoutManager
{
///
/// Use a Thread or a Timer to monitor the ugly
///
private static Thread m_internalThread = null;
private static readonly object m_threadLock = new();
private static readonly ConcurrentQueue m_contexts = new();
private static readonly ConcurrentQueue m_highPrio = new();
private static readonly ConcurrentQueue m_midPrio = new();
private static readonly ConcurrentQueue m_lowPrio = new();
private static AutoResetEvent m_processWaitEven = new(false);
private static bool m_shuttingDown;
private static int m_ActiveSendingCount;
private static double m_lastTimeOutCheckTime = 0;
const int m_maxConcurrentSend = 32;
static ContextTimeoutManager()
{
TimeStampClockPeriod = 1.0 / (double)Stopwatch.Frequency;
TimeStampClockPeriodMS = 1e3 / (double)Stopwatch.Frequency;
}
public static void Start()
{
lock (m_threadLock)
{
if (m_internalThread != null)
return;
m_lastTimeOutCheckTime = GetTimeStamp();
using(ExecutionContext.SuppressFlow())
m_internalThread = new Thread(ThreadRunProcess);
m_internalThread.Priority = ThreadPriority.Normal;
m_internalThread.IsBackground = true;
m_internalThread.CurrentCulture = new CultureInfo("en-US", false);
m_internalThread.Name = "HttpServerMain";
m_internalThread.Start();
}
}
public static void Stop()
{
if (m_processWaitEven != null)
{
m_processWaitEven.Set();
m_shuttingDown = true;
}
}
private static void ThreadRunProcess()
{
while (!m_shuttingDown)
{
m_processWaitEven.WaitOne(500);
if (m_shuttingDown)
break;
double now = GetTimeStamp();
if(!m_contexts.IsEmpty)
{
ProcessSendQueues();
if (m_shuttingDown)
break;
if (now - m_lastTimeOutCheckTime > 1.0)
{
ProcessContextTimeouts();
m_lastTimeOutCheckTime = now;
}
}
else
m_lastTimeOutCheckTime = now;
}
ProcessShutDown();
}
public static void ProcessShutDown()
{
try
{
if(m_processWaitEven != null)
{
SocketError disconnectError = SocketError.HostDown;
for (int i = 0; i < m_contexts.Count; i++)
{
if (m_contexts.TryDequeue(out HttpClientContext context))
{
try
{
context.Disconnect(disconnectError);
}
catch { }
}
}
m_processWaitEven.Dispose();
m_processWaitEven = null;
}
}
catch
{
// We can't let this crash.
}
}
public static void ProcessSendQueues()
{
int inqueues = m_highPrio.Count + m_midPrio.Count + m_lowPrio.Count;
if(inqueues == 0)
return;
const int curbytesLimit = 128 * 1024;
int curConcurrentLimit = m_maxConcurrentSend - m_ActiveSendingCount;
if(curConcurrentLimit <= 0)
return;
if(curConcurrentLimit > inqueues)
curConcurrentLimit = inqueues;
HttpClientContext ctx;
int sentFromQueue;
bool done;
while (curConcurrentLimit > 0)
{
sentFromQueue = 0;
done = true;
while (m_highPrio.TryDequeue(out ctx))
{
if (m_shuttingDown)
return;
done = false;
if (ctx.TrySendResponse(curbytesLimit))
{
--curConcurrentLimit;
if (++sentFromQueue == 3)
break;
}
}
sentFromQueue = 0;
while(m_midPrio.TryDequeue(out ctx))
{
if (m_shuttingDown)
return;
done = false;
if (ctx.TrySendResponse(curbytesLimit))
{
--curConcurrentLimit;
if (++sentFromQueue >= 2)
break;
}
}
if (m_lowPrio.TryDequeue(out ctx))
{
if (m_shuttingDown)
return;
done = false;
if (ctx.TrySendResponse(curbytesLimit))
--curConcurrentLimit;
}
if (done)
break;
}
}
///
/// Causes the watcher to immediately check the connections.
///
public static void ProcessContextTimeouts()
{
try
{
for (int i = 0; i < m_contexts.Count; i++)
{
if (m_shuttingDown)
return;
if (m_contexts.TryDequeue(out HttpClientContext context))
{
if (!ContextTimedOut(context, out SocketError disconnectError))
m_contexts.Enqueue(context);
else if(disconnectError != SocketError.InProgress)
context.Disconnect(disconnectError);
}
}
}
catch
{
// We can't let this crash.
}
}
private static bool ContextTimedOut(HttpClientContext context, out SocketError disconnectError)
{
disconnectError = SocketError.InProgress;
// First our error conditions
if (context.contextID < 0 || context.StopMonitoring || context.StreamPassedOff)
return true;
int nowMS = EnvironmentTickCount();
// First we check first contact line
if (!context.FirstRequestLineReceived)
{
if (EnvironmentTickCountAdd(context.TimeoutFirstLine, context.LastActivityTimeMS) < nowMS)
{
disconnectError = SocketError.TimedOut;
return true;
}
return false;
}
// First we check first contact request
if (!context.FullRequestReceived)
{
if (EnvironmentTickCountAdd(context.TimeoutRequestReceived, context.LastActivityTimeMS) < nowMS)
{
disconnectError = SocketError.TimedOut;
return true;
}
return false;
}
if (context.TriggerKeepalive)
{
context.TriggerKeepalive = false;
context.MonitorKeepaliveStartMS = nowMS + 500;
return false;
}
if (context.MonitorKeepaliveStartMS != 0)
{
if (context.IsClosing)
{
disconnectError = SocketError.Success;
return true;
}
if (EnvironmentTickCountAdd(context.TimeoutKeepAlive, context.MonitorKeepaliveStartMS) < nowMS)
{
disconnectError = SocketError.TimedOut;
context.MonitorKeepaliveStartMS = 0;
return true;
}
}
if (EnvironmentTickCountAdd(context.TimeoutMaxIdle, context.LastActivityTimeMS) < nowMS)
{
disconnectError = SocketError.TimedOut;
context.MonitorKeepaliveStartMS = 0;
return true;
}
return false;
}
public static void StartMonitoringContext(HttpClientContext context)
{
context.LastActivityTimeMS = EnvironmentTickCount();
m_contexts.Enqueue(context);
}
public static void EnqueueSend(HttpClientContext context, int priority)
{
switch(priority)
{
case 0:
m_highPrio.Enqueue(context);
break;
case 1:
m_midPrio.Enqueue(context);
break;
case 2:
m_lowPrio.Enqueue(context);
break;
default:
return;
}
m_processWaitEven?.Set();
}
public static void PulseWaitSend()
{
m_processWaitEven?.Set();
}
public static void ContextEnterActiveSend()
{
Interlocked.Increment(ref m_ActiveSendingCount);
}
public static void ContextLeaveActiveSend()
{
Interlocked.Decrement(ref m_ActiveSendingCount);
}
///
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. This trims down TickCount so it doesn't wrap
/// for the callers.
/// This trims it to a 12 day interval so don't let your frame time get too long.
///
///
public static int EnvironmentTickCount()
{
return Environment.TickCount & EnvironmentTickCountMask;
}
const int EnvironmentTickCountMask = 0x3fffffff;
///
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. Subtracts the passed value (previously fetched by
/// 'EnvironmentTickCount()') and accounts for any wrapping.
///
///
///
/// subtraction of passed prevValue from current Environment.TickCount
public static int EnvironmentTickCountSubtract(Int32 newValue, Int32 prevValue)
{
int diff = newValue - prevValue;
return (diff >= 0) ? diff : (diff + EnvironmentTickCountMask + 1);
}
///
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. Subtracts the passed value (previously fetched by
/// 'EnvironmentTickCount()') and accounts for any wrapping.
///
///
///
/// subtraction of passed prevValue from current Environment.TickCount
public static int EnvironmentTickCountAdd(Int32 newValue, Int32 prevValue)
{
int ret = newValue + prevValue;
return (ret >= 0) ? ret : (ret + EnvironmentTickCountMask + 1);
}
public static double TimeStampClockPeriodMS;
public static double TimeStampClockPeriod;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double GetTimeStamp()
{
return Stopwatch.GetTimestamp() * TimeStampClockPeriod;
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double GetTimeStampMS()
{
return Stopwatch.GetTimestamp() * TimeStampClockPeriodMS;
}
// doing math in ticks is usefull to avoid loss of resolution
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static long GetTimeStampTicks()
{
return Stopwatch.GetTimestamp();
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double TimeStampTicksToMS(long ticks)
{
return ticks * TimeStampClockPeriodMS;
}
}
}