From f3e177814a30ee91a2fdd27f2a1aebf06a39cd15 Mon Sep 17 00:00:00 2001
From: Justin Clark-Casey (justincc)
Date: Mon, 17 Mar 2014 20:51:35 +0000
Subject: Add regression test for http inventory fetch.
Involved some restructuring to allow regression tests to dequeue inventory requests and perform poll responses synchronously rather than async
---
OpenSim/Capabilities/Caps.cs | 7 +-
.../Framework/Servers/HttpServer/BaseHttpServer.cs | 24 +-
.../HttpServer/PollServiceRequestManager.cs | 175 ++++++-----
OpenSim/Framework/Servers/Tests/OSHttpTests.cs | 320 +--------------------
.../Caps/Tests/WebFetchInvDescModuleTests.cs | 158 ++++++++++
.../Linden/Caps/WebFetchInvDescModule.cs | 43 ++-
.../Framework/Caps/CapabilitiesModule.cs | 4 +
OpenSim/Tests/Common/Mock/TestHttpClientContext.cs | 110 +++++++
OpenSim/Tests/Common/Mock/TestHttpRequest.cs | 174 +++++++++++
OpenSim/Tests/Common/Mock/TestHttpResponse.cs | 171 +++++++++++
10 files changed, 780 insertions(+), 406 deletions(-)
create mode 100644 OpenSim/Region/ClientStack/Linden/Caps/Tests/WebFetchInvDescModuleTests.cs
create mode 100644 OpenSim/Tests/Common/Mock/TestHttpClientContext.cs
create mode 100644 OpenSim/Tests/Common/Mock/TestHttpRequest.cs
create mode 100644 OpenSim/Tests/Common/Mock/TestHttpResponse.cs
(limited to 'OpenSim')
diff --git a/OpenSim/Capabilities/Caps.cs b/OpenSim/Capabilities/Caps.cs
index bbf3b27..049afab 100644
--- a/OpenSim/Capabilities/Caps.cs
+++ b/OpenSim/Capabilities/Caps.cs
@@ -50,8 +50,7 @@ namespace OpenSim.Framework.Capabilities
public class Caps
{
-// private static readonly ILog m_log =
-// LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private string m_httpListenerHostName;
private uint m_httpListenPort;
@@ -152,6 +151,10 @@ namespace OpenSim.Framework.Capabilities
public void RegisterPollHandler(string capName, PollServiceEventArgs pollServiceHandler)
{
+// m_log.DebugFormat(
+// "[CAPS]: Registering handler with name {0}, url {1} for {2}",
+// capName, pollServiceHandler.Url, m_agentID, m_regionName);
+
m_pollServiceHandlers.Add(capName, pollServiceHandler);
m_httpListener.AddPollServiceHTTPHandler(pollServiceHandler.Url, pollServiceHandler);
diff --git a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
index e1ae74e..e243002 100644
--- a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
+++ b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
@@ -113,7 +113,7 @@ namespace OpenSim.Framework.Servers.HttpServer
protected IPAddress m_listenIPAddress = IPAddress.Any;
- private PollServiceRequestManager m_PollServiceManager;
+ public PollServiceRequestManager PollServiceRequestManager { get; private set; }
public uint SSLPort
{
@@ -374,7 +374,7 @@ namespace OpenSim.Framework.Servers.HttpServer
return true;
}
- private void OnRequest(object source, RequestEventArgs args)
+ public void OnRequest(object source, RequestEventArgs args)
{
RequestNumber++;
@@ -429,7 +429,7 @@ namespace OpenSim.Framework.Servers.HttpServer
psEvArgs.Request(psreq.RequestID, keysvals);
}
- m_PollServiceManager.Enqueue(psreq);
+ PollServiceRequestManager.Enqueue(psreq);
}
else
{
@@ -1781,10 +1781,17 @@ namespace OpenSim.Framework.Servers.HttpServer
public void Start()
{
- StartHTTP();
+ Start(true);
}
- private void StartHTTP()
+ ///
+ /// Start the http server
+ ///
+ ///
+ /// If true then poll responses are performed asynchronsly.
+ /// Option exists to allow regression tests to perform processing synchronously.
+ ///
+ public void Start(bool performPollResponsesAsync)
{
m_log.InfoFormat(
"[BASE HTTP SERVER]: Starting {0} server on port {1}", UseSSL ? "HTTPS" : "HTTP", Port);
@@ -1822,8 +1829,9 @@ namespace OpenSim.Framework.Servers.HttpServer
m_httpListener2.Start(64);
// Long Poll Service Manager with 3 worker threads a 25 second timeout for no events
- m_PollServiceManager = new PollServiceRequestManager(this, 3, 25000);
- m_PollServiceManager.Start();
+ PollServiceRequestManager = new PollServiceRequestManager(this, performPollResponsesAsync, 3, 25000);
+ PollServiceRequestManager.Start();
+
HTTPDRunning = true;
//HttpListenerContext context;
@@ -1892,7 +1900,7 @@ namespace OpenSim.Framework.Servers.HttpServer
try
{
- m_PollServiceManager.Stop();
+ PollServiceRequestManager.Stop();
m_httpListener2.ExceptionThrown -= httpServerException;
//m_httpListener2.DisconnectHandler = null;
diff --git a/OpenSim/Framework/Servers/HttpServer/PollServiceRequestManager.cs b/OpenSim/Framework/Servers/HttpServer/PollServiceRequestManager.cs
index 6aa5907..456acb0 100644
--- a/OpenSim/Framework/Servers/HttpServer/PollServiceRequestManager.cs
+++ b/OpenSim/Framework/Servers/HttpServer/PollServiceRequestManager.cs
@@ -44,6 +44,20 @@ namespace OpenSim.Framework.Servers.HttpServer
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+ ///
+ /// Is the poll service request manager running?
+ ///
+ ///
+ /// Can be running either synchronously or asynchronously
+ ///
+ public bool IsRunning { get; private set; }
+
+ ///
+ /// Is the poll service performing responses asynchronously (with its own threads) or synchronously (via
+ /// external calls)?
+ ///
+ public bool PerformResponsesAsync { get; private set; }
+
private readonly BaseHttpServer m_server;
private BlockingQueue m_requests = new BlockingQueue();
@@ -52,48 +66,53 @@ namespace OpenSim.Framework.Servers.HttpServer
private uint m_WorkerThreadCount = 0;
private Thread[] m_workerThreads;
- private bool m_running = true;
-
private SmartThreadPool m_threadPool = new SmartThreadPool(20000, 12, 2);
// private int m_timeout = 1000; // increase timeout 250; now use the event one
- public PollServiceRequestManager(BaseHttpServer pSrv, uint pWorkerThreadCount, int pTimeout)
+ public PollServiceRequestManager(
+ BaseHttpServer pSrv, bool performResponsesAsync, uint pWorkerThreadCount, int pTimeout)
{
m_server = pSrv;
+ PerformResponsesAsync = performResponsesAsync;
m_WorkerThreadCount = pWorkerThreadCount;
m_workerThreads = new Thread[m_WorkerThreadCount];
}
public void Start()
{
- //startup worker threads
- for (uint i = 0; i < m_WorkerThreadCount; i++)
+ IsRunning = true;
+
+ if (PerformResponsesAsync)
{
- m_workerThreads[i]
- = Watchdog.StartThread(
- PoolWorkerJob,
- string.Format("PollServiceWorkerThread{0}:{1}", i, m_server.Port),
- ThreadPriority.Normal,
- false,
- false,
- null,
- int.MaxValue);
- }
+ //startup worker threads
+ for (uint i = 0; i < m_WorkerThreadCount; i++)
+ {
+ m_workerThreads[i]
+ = Watchdog.StartThread(
+ PoolWorkerJob,
+ string.Format("PollServiceWorkerThread{0}:{1}", i, m_server.Port),
+ ThreadPriority.Normal,
+ false,
+ false,
+ null,
+ int.MaxValue);
+ }
- Watchdog.StartThread(
- this.CheckLongPollThreads,
- string.Format("LongPollServiceWatcherThread:{0}", m_server.Port),
- ThreadPriority.Normal,
- false,
- true,
- null,
- 1000 * 60 * 10);
+ Watchdog.StartThread(
+ this.CheckLongPollThreads,
+ string.Format("LongPollServiceWatcherThread:{0}", m_server.Port),
+ ThreadPriority.Normal,
+ false,
+ true,
+ null,
+ 1000 * 60 * 10);
+ }
}
private void ReQueueEvent(PollServiceHttpRequest req)
{
- if (m_running)
+ if (IsRunning)
{
// delay the enqueueing for 100ms. There's no need to have the event
// actively on the queue
@@ -109,7 +128,7 @@ namespace OpenSim.Framework.Servers.HttpServer
public void Enqueue(PollServiceHttpRequest req)
{
- if (m_running)
+ if (IsRunning)
{
if (req.PollServiceArgs.Type == PollServiceEventArgs.EventType.LongPoll)
{
@@ -129,7 +148,7 @@ namespace OpenSim.Framework.Servers.HttpServer
// All other types of tasks (Inventory handlers, http-in, etc) don't have the long-poll nature,
// so if they aren't ready to be served by a worker thread (no events), they are placed
// directly back in the "ready-to-serve" queue by the worker thread.
- while (m_running)
+ while (IsRunning)
{
Thread.Sleep(500);
Watchdog.UpdateThread();
@@ -137,7 +156,7 @@ namespace OpenSim.Framework.Servers.HttpServer
// List not_ready = new List();
lock (m_longPollRequests)
{
- if (m_longPollRequests.Count > 0 && m_running)
+ if (m_longPollRequests.Count > 0 && IsRunning)
{
List ready = m_longPollRequests.FindAll(req =>
(req.PollServiceArgs.HasEvents(req.RequestID, req.PollServiceArgs.Id) || // there are events in this EQ
@@ -158,7 +177,7 @@ namespace OpenSim.Framework.Servers.HttpServer
public void Stop()
{
- m_running = false;
+ IsRunning = false;
// m_timeout = -10000; // cause all to expire
Thread.Sleep(1000); // let the world move
@@ -169,7 +188,7 @@ namespace OpenSim.Framework.Servers.HttpServer
lock (m_longPollRequests)
{
- if (m_longPollRequests.Count > 0 && m_running)
+ if (m_longPollRequests.Count > 0 && IsRunning)
m_longPollRequests.ForEach(req => m_requests.Enqueue(req));
}
@@ -194,68 +213,82 @@ namespace OpenSim.Framework.Servers.HttpServer
private void PoolWorkerJob()
{
- while (m_running)
+ while (IsRunning)
{
- PollServiceHttpRequest req = m_requests.Dequeue(5000);
- //m_log.WarnFormat("[YYY]: Dequeued {0}", (req == null ? "null" : req.PollServiceArgs.Type.ToString()));
-
Watchdog.UpdateThread();
- if (req != null)
+ WaitPerformResponse();
+ }
+ }
+
+ public void WaitPerformResponse()
+ {
+ PollServiceHttpRequest req = m_requests.Dequeue(5000);
+// m_log.DebugFormat("[YYY]: Dequeued {0}", (req == null ? "null" : req.PollServiceArgs.Type.ToString()));
+
+ if (req != null)
+ {
+ try
{
- try
+ if (req.PollServiceArgs.HasEvents(req.RequestID, req.PollServiceArgs.Id))
{
- if (req.PollServiceArgs.HasEvents(req.RequestID, req.PollServiceArgs.Id))
- {
- Hashtable responsedata = req.PollServiceArgs.GetEvents(req.RequestID, req.PollServiceArgs.Id);
+ Hashtable responsedata = req.PollServiceArgs.GetEvents(req.RequestID, req.PollServiceArgs.Id);
- if (responsedata == null)
- continue;
+ if (responsedata == null)
+ return;
- if (req.PollServiceArgs.Type == PollServiceEventArgs.EventType.LongPoll) // This is the event queue
+ // This is the event queue.
+ // Even if we're not running we can still perform responses by explicit request.
+ if (req.PollServiceArgs.Type == PollServiceEventArgs.EventType.LongPoll
+ || !PerformResponsesAsync)
+ {
+ try
+ {
+ req.DoHTTPGruntWork(m_server, responsedata);
+ }
+ catch (ObjectDisposedException e) // Browser aborted before we could read body, server closed the stream
+ {
+ // Ignore it, no need to reply
+ m_log.Error(e);
+ }
+ }
+ else
+ {
+ m_threadPool.QueueWorkItem(x =>
{
try
{
req.DoHTTPGruntWork(m_server, responsedata);
}
- catch (ObjectDisposedException) // Browser aborted before we could read body, server closed the stream
+ catch (ObjectDisposedException e) // Browser aborted before we could read body, server closed the stream
{
// Ignore it, no need to reply
+ m_log.Error(e);
}
- }
- else
- {
- m_threadPool.QueueWorkItem(x =>
+ catch (Exception e)
{
- try
- {
- req.DoHTTPGruntWork(m_server, responsedata);
- }
- catch (ObjectDisposedException) // Browser aborted before we could read body, server closed the stream
- {
- // Ignore it, no need to reply
- }
-
- return null;
- }, null);
- }
+ m_log.Error(e);
+ }
+
+ return null;
+ }, null);
+ }
+ }
+ else
+ {
+ if ((Environment.TickCount - req.RequestTime) > req.PollServiceArgs.TimeOutms)
+ {
+ req.DoHTTPGruntWork(
+ m_server, req.PollServiceArgs.NoEvents(req.RequestID, req.PollServiceArgs.Id));
}
else
{
- if ((Environment.TickCount - req.RequestTime) > req.PollServiceArgs.TimeOutms)
- {
- req.DoHTTPGruntWork(
- m_server, req.PollServiceArgs.NoEvents(req.RequestID, req.PollServiceArgs.Id));
- }
- else
- {
- ReQueueEvent(req);
- }
+ ReQueueEvent(req);
}
}
- catch (Exception e)
- {
- m_log.ErrorFormat("Exception in poll service thread: " + e.ToString());
- }
+ }
+ catch (Exception e)
+ {
+ m_log.ErrorFormat("Exception in poll service thread: " + e.ToString());
}
}
}
diff --git a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs b/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
index 5b912b4..5c0e0df 100644
--- a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
+++ b/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
@@ -41,323 +41,7 @@ namespace OpenSim.Framework.Servers.Tests
{
[TestFixture]
public class OSHttpTests : OpenSimTestCase
- {
- // we need an IHttpClientContext for our tests
- public class TestHttpClientContext: IHttpClientContext
- {
- private bool _secured;
- public bool IsSecured
- {
- get { return _secured; }
- }
- public bool Secured
- {
- get { return _secured; }
- }
-
- public TestHttpClientContext(bool secured)
- {
- _secured = secured;
- }
-
- public void Disconnect(SocketError error) {}
- public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body) {}
- public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) {}
- public void Respond(string body) {}
- public void Send(byte[] buffer) {}
- public void Send(byte[] buffer, int offset, int size) {}
- public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType) {}
- public void Close() { }
- public bool EndWhenDone { get { return false;} set { return;}}
-
- public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing()
- {
- return new HTTPNetworkContext();
- }
-
- public event EventHandler Disconnected = delegate { };
- ///
- /// A request have been received in the context.
- ///
- public event EventHandler RequestReceived = delegate { };
-
- }
-
- public class TestHttpRequest: IHttpRequest
- {
- private string _uriPath;
- public bool BodyIsComplete
- {
- get { return true; }
- }
- public string[] AcceptTypes
- {
- get {return _acceptTypes; }
- }
- private string[] _acceptTypes;
- public Stream Body
- {
- get { return _body; }
- set { _body = value;}
- }
- private Stream _body;
- public ConnectionType Connection
- {
- get { return _connection; }
- set { _connection = value; }
- }
- private ConnectionType _connection;
- public int ContentLength
- {
- get { return _contentLength; }
- set { _contentLength = value; }
- }
- private int _contentLength;
- public NameValueCollection Headers
- {
- get { return _headers; }
- }
- private NameValueCollection _headers = new NameValueCollection();
- public string HttpVersion
- {
- get { return _httpVersion; }
- set { _httpVersion = value; }
- }
- private string _httpVersion = null;
- public string Method
- {
- get { return _method; }
- set { _method = value; }
- }
- private string _method = null;
- public HttpInput QueryString
- {
- get { return _queryString; }
- }
- private HttpInput _queryString = null;
- public Uri Uri
- {
- get { return _uri; }
- set { _uri = value; }
- }
- private Uri _uri = null;
- public string[] UriParts
- {
- get { return _uri.Segments; }
- }
- public HttpParam Param
- {
- get { return null; }
- }
- public HttpForm Form
- {
- get { return null; }
- }
- public bool IsAjax
- {
- get { return false; }
- }
- public RequestCookies Cookies
- {
- get { return null; }
- }
-
- public TestHttpRequest() {}
-
- public TestHttpRequest(string contentEncoding, string contentType, string userAgent,
- string remoteAddr, string remotePort, string[] acceptTypes,
- ConnectionType connectionType, int contentLength, Uri uri)
- {
- _headers["content-encoding"] = contentEncoding;
- _headers["content-type"] = contentType;
- _headers["user-agent"] = userAgent;
- _headers["remote_addr"] = remoteAddr;
- _headers["remote_port"] = remotePort;
-
- _acceptTypes = acceptTypes;
- _connection = connectionType;
- _contentLength = contentLength;
- _uri = uri;
- }
-
- public void DecodeBody(FormDecoderProvider providers) {}
- public void SetCookies(RequestCookies cookies) {}
- public void AddHeader(string name, string value)
- {
- _headers.Add(name, value);
- }
- public int AddToBody(byte[] bytes, int offset, int length)
- {
- return 0;
- }
- public void Clear() {}
-
- public object Clone()
- {
- TestHttpRequest clone = new TestHttpRequest();
- clone._acceptTypes = _acceptTypes;
- clone._connection = _connection;
- clone._contentLength = _contentLength;
- clone._uri = _uri;
- clone._headers = new NameValueCollection(_headers);
-
- return clone;
- }
- public IHttpResponse CreateResponse(IHttpClientContext context)
- {
- return new HttpResponse(context, this);
- }
- ///
- /// Path and query (will be merged with the host header) and put in Uri
- ///
- ///
- public string UriPath
- {
- get { return _uriPath; }
- set
- {
- _uriPath = value;
-
- }
- }
-
- }
-
- public class TestHttpResponse: IHttpResponse
- {
- public Stream Body
- {
- get { return _body; }
-
- set { _body = value; }
- }
- private Stream _body;
-
- public string ProtocolVersion
- {
- get { return _protocolVersion; }
- set { _protocolVersion = value; }
- }
- private string _protocolVersion;
-
- public bool Chunked
- {
- get { return _chunked; }
-
- set { _chunked = value; }
- }
- private bool _chunked;
-
- public ConnectionType Connection
- {
- get { return _connection; }
-
- set { _connection = value; }
- }
- private ConnectionType _connection;
-
- public Encoding Encoding
- {
- get { return _encoding; }
-
- set { _encoding = value; }
- }
- private Encoding _encoding;
-
- public int KeepAlive
- {
- get { return _keepAlive; }
-
- set { _keepAlive = value; }
- }
- private int _keepAlive;
-
- public HttpStatusCode Status
- {
- get { return _status; }
-
- set { _status = value; }
- }
- private HttpStatusCode _status;
-
- public string Reason
- {
- get { return _reason; }
-
- set { _reason = value; }
- }
- private string _reason;
-
- public long ContentLength
- {
- get { return _contentLength; }
-
- set { _contentLength = value; }
- }
- private long _contentLength;
-
- public string ContentType
- {
- get { return _contentType; }
-
- set { _contentType = value; }
- }
- private string _contentType;
-
- public bool HeadersSent
- {
- get { return _headersSent; }
- }
- private bool _headersSent;
-
- public bool Sent
- {
- get { return _sent; }
- }
- private bool _sent;
-
- public ResponseCookies Cookies
- {
- get { return _cookies; }
- }
- private ResponseCookies _cookies = null;
-
- public TestHttpResponse()
- {
- _headersSent = false;
- _sent = false;
- }
-
- public void AddHeader(string name, string value) {}
- public void Send()
- {
- if (!_headersSent) SendHeaders();
- if (_sent) throw new InvalidOperationException("stuff already sent");
- _sent = true;
- }
-
- public void SendBody(byte[] buffer, int offset, int count)
- {
- if (!_headersSent) SendHeaders();
- _sent = true;
- }
- public void SendBody(byte[] buffer)
- {
- if (!_headersSent) SendHeaders();
- _sent = true;
- }
-
- public void SendHeaders()
- {
- if (_headersSent) throw new InvalidOperationException("headers already sent");
- _headersSent = true;
- }
-
- public void Redirect(Uri uri) {}
- public void Redirect(string url) {}
- }
-
-
+ {
public OSHttpRequest req0;
public OSHttpRequest req1;
@@ -429,4 +113,4 @@ namespace OpenSim.Framework.Servers.Tests
Assert.That(rsp0.ContentType, Is.EqualTo("text/xml"));
}
}
-}
+}
\ No newline at end of file
diff --git a/OpenSim/Region/ClientStack/Linden/Caps/Tests/WebFetchInvDescModuleTests.cs b/OpenSim/Region/ClientStack/Linden/Caps/Tests/WebFetchInvDescModuleTests.cs
new file mode 100644
index 0000000..edc5016
--- /dev/null
+++ b/OpenSim/Region/ClientStack/Linden/Caps/Tests/WebFetchInvDescModuleTests.cs
@@ -0,0 +1,158 @@
+/*
+ * 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using HttpServer;
+using log4net.Config;
+using Nini.Config;
+using NUnit.Framework;
+using OpenMetaverse;
+using OpenMetaverse.Packets;
+using OpenMetaverse.StructuredData;
+using OpenSim.Framework;
+using OpenSim.Framework.Capabilities;
+using OpenSim.Framework.Servers;
+using OpenSim.Framework.Servers.HttpServer;
+using OpenSim.Region.ClientStack.Linden;
+using OpenSim.Region.CoreModules.Framework;
+using OpenSim.Region.Framework.Scenes;
+using OpenSim.Services.Interfaces;
+using OpenSim.Tests.Common;
+using OpenSim.Tests.Common.Mock;
+using OSDArray = OpenMetaverse.StructuredData.OSDArray;
+using OSDMap = OpenMetaverse.StructuredData.OSDMap;
+
+namespace OpenSim.Region.ClientStack.Linden.Caps.Tests
+{
+ [TestFixture]
+ public class WebFetchInvDescModuleTests : OpenSimTestCase
+ {
+ [TestFixtureSetUp]
+ public void TestFixtureSetUp()
+ {
+ // Don't allow tests to be bamboozled by asynchronous events. Execute everything on the same thread.
+ Util.FireAndForgetMethod = FireAndForgetMethod.RegressionTest;
+ }
+
+ [TestFixtureTearDown]
+ public void TestFixureTearDown()
+ {
+ // We must set this back afterwards, otherwise later tests will fail since they're expecting multiple
+ // threads. Possibly, later tests should be rewritten so none of them require async stuff (which regression
+ // tests really shouldn't).
+ Util.FireAndForgetMethod = Util.DefaultFireAndForgetMethod;
+ }
+
+ [SetUp]
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ // This is an unfortunate bit of clean up we have to do because MainServer manages things through static
+ // variables and the VM is not restarted between tests.
+ uint port = 9999;
+ MainServer.RemoveHttpServer(port);
+
+ BaseHttpServer server = new BaseHttpServer(port, false, 0, "");
+ MainServer.AddHttpServer(server);
+ MainServer.Instance = server;
+
+ server.Start(false);
+ }
+
+ [Test]
+ public void TestInventoryDescendentsFetch()
+ {
+ TestHelpers.InMethod();
+ TestHelpers.EnableLogging();
+
+ BaseHttpServer httpServer = MainServer.Instance;
+ Scene scene = new SceneHelpers().SetupScene();
+
+ CapabilitiesModule capsModule = new CapabilitiesModule();
+ WebFetchInvDescModule wfidModule = new WebFetchInvDescModule(false);
+
+ IConfigSource config = new IniConfigSource();
+ config.AddConfig("ClientStack.LindenCaps");
+ config.Configs["ClientStack.LindenCaps"].Set("Cap_FetchInventoryDescendents2", "localhost");
+
+ SceneHelpers.SetupSceneModules(scene, config, capsModule, wfidModule);
+
+ UserAccount ua = UserAccountHelpers.CreateUserWithInventory(scene, TestHelpers.ParseTail(0x1));
+
+ // We need a user present to have any capabilities set up
+ SceneHelpers.AddScenePresence(scene, ua.PrincipalID);
+
+ TestHttpRequest req = new TestHttpRequest();
+ OpenSim.Framework.Capabilities.Caps userCaps = capsModule.GetCapsForUser(ua.PrincipalID);
+ PollServiceEventArgs pseArgs;
+ userCaps.TryGetPollHandler("FetchInventoryDescendents2", out pseArgs);
+ req.UriPath = pseArgs.Url;
+ req.Uri = new Uri(req.UriPath);
+
+ // Retrieve root folder details directly so that we can request
+ InventoryFolderBase folder = scene.InventoryService.GetRootFolder(ua.PrincipalID);
+
+ OSDMap osdFolder = new OSDMap();
+ osdFolder["folder_id"] = folder.ID;
+ osdFolder["owner_id"] = ua.PrincipalID;
+ osdFolder["fetch_folders"] = true;
+ osdFolder["fetch_items"] = true;
+ osdFolder["sort_order"] = 0;
+
+ OSDArray osdFoldersArray = new OSDArray();
+ osdFoldersArray.Add(osdFolder);
+
+ OSDMap osdReqMap = new OSDMap();
+ osdReqMap["folders"] = osdFoldersArray;
+
+ req.Body = new MemoryStream(OSDParser.SerializeLLSDXmlBytes(osdReqMap));
+
+ TestHttpClientContext context = new TestHttpClientContext(false);
+ MainServer.Instance.OnRequest(context, new RequestEventArgs(req));
+
+ // Drive processing of the queued inventory request synchronously.
+ wfidModule.WaitProcessQueuedInventoryRequest();
+ MainServer.Instance.PollServiceRequestManager.WaitPerformResponse();
+
+// System.Threading.Thread.Sleep(10000);
+
+ OSDMap responseOsd = (OSDMap)OSDParser.DeserializeLLSDXml(context.ResponseBody);
+ OSDArray foldersOsd = (OSDArray)responseOsd["folders"];
+ OSDMap folderOsd = (OSDMap)foldersOsd[0];
+
+ // A sanity check that the response has the expected number of descendents for a default inventory
+ // TODO: Need a more thorough check.
+ Assert.That((int)folderOsd["descendents"], Is.EqualTo(14));
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenSim/Region/ClientStack/Linden/Caps/WebFetchInvDescModule.cs b/OpenSim/Region/ClientStack/Linden/Caps/WebFetchInvDescModule.cs
index 340d2e7..f0dccda 100644
--- a/OpenSim/Region/ClientStack/Linden/Caps/WebFetchInvDescModule.cs
+++ b/OpenSim/Region/ClientStack/Linden/Caps/WebFetchInvDescModule.cs
@@ -65,6 +65,15 @@ namespace OpenSim.Region.ClientStack.Linden
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+
+ ///
+ /// Control whether requests will be processed asynchronously.
+ ///
+ ///
+ /// Defaults to true. Can currently not be changed once a region has been added to the module.
+ ///
+ public bool ProcessQueuedRequestsAsync { get; private set; }
+
private Scene m_scene;
private IInventoryService m_InventoryService;
@@ -84,6 +93,13 @@ namespace OpenSim.Region.ClientStack.Linden
#region ISharedRegionModule Members
+ public WebFetchInvDescModule() : this(true) {}
+
+ public WebFetchInvDescModule(bool processQueuedResultsAsync)
+ {
+ ProcessQueuedRequestsAsync = processQueuedResultsAsync;
+ }
+
public void Initialise(IConfigSource source)
{
IConfig config = source.Configs["ClientStack.LindenCaps"];
@@ -114,8 +130,16 @@ namespace OpenSim.Region.ClientStack.Linden
m_scene.EventManager.OnRegisterCaps -= RegisterCaps;
- foreach (Thread t in m_workerThreads)
- Watchdog.AbortThread(t.ManagedThreadId);
+ if (ProcessQueuedRequestsAsync)
+ {
+ if (m_workerThreads != null)
+ {
+ foreach (Thread t in m_workerThreads)
+ Watchdog.AbortThread(t.ManagedThreadId);
+
+ m_workerThreads = null;
+ }
+ }
m_scene = null;
}
@@ -133,7 +157,7 @@ namespace OpenSim.Region.ClientStack.Linden
m_scene.EventManager.OnRegisterCaps += RegisterCaps;
- if (m_workerThreads == null)
+ if (ProcessQueuedRequestsAsync && m_workerThreads == null)
{
m_workerThreads = new Thread[2];
@@ -358,11 +382,16 @@ namespace OpenSim.Region.ClientStack.Linden
{
Watchdog.UpdateThread();
- aPollRequest poolreq = m_queue.Dequeue();
-
- if (poolreq != null && poolreq.thepoll != null)
- poolreq.thepoll.Process(poolreq);
+ WaitProcessQueuedInventoryRequest();
}
}
+
+ public void WaitProcessQueuedInventoryRequest()
+ {
+ aPollRequest poolreq = m_queue.Dequeue();
+
+ if (poolreq != null && poolreq.thepoll != null)
+ poolreq.thepoll.Process(poolreq);
+ }
}
}
diff --git a/OpenSim/Region/CoreModules/Framework/Caps/CapabilitiesModule.cs b/OpenSim/Region/CoreModules/Framework/Caps/CapabilitiesModule.cs
index 13cc99a..817ef85 100644
--- a/OpenSim/Region/CoreModules/Framework/Caps/CapabilitiesModule.cs
+++ b/OpenSim/Region/CoreModules/Framework/Caps/CapabilitiesModule.cs
@@ -137,6 +137,10 @@ namespace OpenSim.Region.CoreModules.Framework
// agentId, m_scene.RegionInfo.RegionName, oldCaps.CapsObjectPath, capsObjectPath);
}
+// m_log.DebugFormat(
+// "[CAPS]: Adding capabilities for agent {0} in {1} with path {2}",
+// agentId, m_scene.RegionInfo.RegionName, capsObjectPath);
+
caps = new Caps(MainServer.Instance, m_scene.RegionInfo.ExternalHostName,
(MainServer.Instance == null) ? 0: MainServer.Instance.Port,
capsObjectPath, agentId, m_scene.RegionInfo.RegionName);
diff --git a/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs b/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs
new file mode 100644
index 0000000..5a55b09
--- /dev/null
+++ b/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs
@@ -0,0 +1,110 @@
+/*
+ * 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.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using HttpServer;
+using OpenSim.Framework;
+
+namespace OpenSim.Tests.Common
+{
+ public class TestHttpClientContext: IHttpClientContext
+ {
+ ///
+ /// Bodies of responses from the server.
+ ///
+ public string ResponseBody
+ {
+ get { return Encoding.UTF8.GetString(m_responseStream.ToArray()); }
+ }
+
+ public Byte[] ResponseBodyBytes
+ {
+ get{ return m_responseStream.ToArray(); }
+ }
+
+ private MemoryStream m_responseStream = new MemoryStream();
+
+ public bool IsSecured { get; set; }
+
+ public bool Secured
+ {
+ get { return IsSecured; }
+ set { IsSecured = value; }
+ }
+
+ public TestHttpClientContext(bool secured)
+ {
+ Secured = secured;
+ }
+
+ public void Disconnect(SocketError error)
+ {
+// Console.WriteLine("TestHttpClientContext.Disconnect Received disconnect with status {0}", error);
+ }
+
+ public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body) {Console.WriteLine("x");}
+ public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) {Console.WriteLine("xx");}
+ public void Respond(string body) { Console.WriteLine("xxx");}
+
+ public void Send(byte[] buffer)
+ {
+ // Getting header data here
+// Console.WriteLine("xxxx: Got {0}", Encoding.UTF8.GetString(buffer));
+ }
+
+ public void Send(byte[] buffer, int offset, int size)
+ {
+// Util.PrintCallStack();
+//
+// Console.WriteLine(
+// "TestHttpClientContext.Send(byte[], int, int) got offset={0}, size={1}, buffer={2}",
+// offset, size, Encoding.UTF8.GetString(buffer));
+
+ m_responseStream.Write(buffer, offset, size);
+ }
+
+ public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType) {Console.WriteLine("xxxxxx");}
+ public void Close() { }
+ public bool EndWhenDone { get { return false;} set { return;}}
+
+ public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing()
+ {
+ return new HTTPNetworkContext();
+ }
+
+ public event EventHandler Disconnected = delegate { };
+ ///
+ /// A request have been received in the context.
+ ///
+ public event EventHandler RequestReceived = delegate { };
+ }
+}
\ No newline at end of file
diff --git a/OpenSim/Tests/Common/Mock/TestHttpRequest.cs b/OpenSim/Tests/Common/Mock/TestHttpRequest.cs
new file mode 100644
index 0000000..b868895
--- /dev/null
+++ b/OpenSim/Tests/Common/Mock/TestHttpRequest.cs
@@ -0,0 +1,174 @@
+/*
+ * 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.Specialized;
+using System.IO;
+using HttpServer;
+using HttpServer.FormDecoders;
+
+namespace OpenSim.Tests.Common
+{
+ public class TestHttpRequest: IHttpRequest
+ {
+ private string _uriPath;
+ public bool BodyIsComplete
+ {
+ get { return true; }
+ }
+ public string[] AcceptTypes
+ {
+ get {return _acceptTypes; }
+ }
+ private string[] _acceptTypes;
+ public Stream Body
+ {
+ get { return _body; }
+ set { _body = value;}
+ }
+ private Stream _body;
+ public ConnectionType Connection
+ {
+ get { return _connection; }
+ set { _connection = value; }
+ }
+ private ConnectionType _connection;
+ public int ContentLength
+ {
+ get { return _contentLength; }
+ set { _contentLength = value; }
+ }
+ private int _contentLength;
+ public NameValueCollection Headers
+ {
+ get { return _headers; }
+ }
+ private NameValueCollection _headers = new NameValueCollection();
+
+ public string HttpVersion { get; set; }
+
+ public string Method
+ {
+ get { return _method; }
+ set { _method = value; }
+ }
+ private string _method = null;
+ public HttpInput QueryString
+ {
+ get { return _queryString; }
+ }
+ private HttpInput _queryString = null;
+ public Uri Uri
+ {
+ get { return _uri; }
+ set { _uri = value; }
+ }
+ private Uri _uri = null;
+ public string[] UriParts
+ {
+ get { return _uri.Segments; }
+ }
+ public HttpParam Param
+ {
+ get { return null; }
+ }
+ public HttpForm Form
+ {
+ get { return null; }
+ }
+ public bool IsAjax
+ {
+ get { return false; }
+ }
+ public RequestCookies Cookies
+ {
+ get { return null; }
+ }
+
+ public TestHttpRequest()
+ {
+ HttpVersion = "HTTP/1.1";
+ }
+
+ public TestHttpRequest(string contentEncoding, string contentType, string userAgent,
+ string remoteAddr, string remotePort, string[] acceptTypes,
+ ConnectionType connectionType, int contentLength, Uri uri) : base()
+ {
+ _headers["content-encoding"] = contentEncoding;
+ _headers["content-type"] = contentType;
+ _headers["user-agent"] = userAgent;
+ _headers["remote_addr"] = remoteAddr;
+ _headers["remote_port"] = remotePort;
+
+ _acceptTypes = acceptTypes;
+ _connection = connectionType;
+ _contentLength = contentLength;
+ _uri = uri;
+ }
+
+ public void DecodeBody(FormDecoderProvider providers) {}
+ public void SetCookies(RequestCookies cookies) {}
+ public void AddHeader(string name, string value)
+ {
+ _headers.Add(name, value);
+ }
+ public int AddToBody(byte[] bytes, int offset, int length)
+ {
+ return 0;
+ }
+ public void Clear() {}
+
+ public object Clone()
+ {
+ TestHttpRequest clone = new TestHttpRequest();
+ clone._acceptTypes = _acceptTypes;
+ clone._connection = _connection;
+ clone._contentLength = _contentLength;
+ clone._uri = _uri;
+ clone._headers = new NameValueCollection(_headers);
+
+ return clone;
+ }
+ public IHttpResponse CreateResponse(IHttpClientContext context)
+ {
+ return new HttpResponse(context, this);
+ }
+ ///
+ /// Path and query (will be merged with the host header) and put in Uri
+ ///
+ ///
+ public string UriPath
+ {
+ get { return _uriPath; }
+ set
+ {
+ _uriPath = value;
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenSim/Tests/Common/Mock/TestHttpResponse.cs b/OpenSim/Tests/Common/Mock/TestHttpResponse.cs
new file mode 100644
index 0000000..ff47c10
--- /dev/null
+++ b/OpenSim/Tests/Common/Mock/TestHttpResponse.cs
@@ -0,0 +1,171 @@
+/*
+ * 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.IO;
+using System.Net;
+using System.Text;
+using HttpServer;
+
+namespace OpenSim.Tests.Common
+{
+ public class TestHttpResponse: IHttpResponse
+ {
+ public Stream Body
+ {
+ get { return _body; }
+
+ set { _body = value; }
+ }
+ private Stream _body;
+
+ public string ProtocolVersion
+ {
+ get { return _protocolVersion; }
+ set { _protocolVersion = value; }
+ }
+ private string _protocolVersion;
+
+ public bool Chunked
+ {
+ get { return _chunked; }
+
+ set { _chunked = value; }
+ }
+ private bool _chunked;
+
+ public ConnectionType Connection
+ {
+ get { return _connection; }
+
+ set { _connection = value; }
+ }
+ private ConnectionType _connection;
+
+ public Encoding Encoding
+ {
+ get { return _encoding; }
+
+ set { _encoding = value; }
+ }
+ private Encoding _encoding;
+
+ public int KeepAlive
+ {
+ get { return _keepAlive; }
+
+ set { _keepAlive = value; }
+ }
+ private int _keepAlive;
+
+ public HttpStatusCode Status
+ {
+ get { return _status; }
+
+ set { _status = value; }
+ }
+ private HttpStatusCode _status;
+
+ public string Reason
+ {
+ get { return _reason; }
+
+ set { _reason = value; }
+ }
+ private string _reason;
+
+ public long ContentLength
+ {
+ get { return _contentLength; }
+
+ set { _contentLength = value; }
+ }
+ private long _contentLength;
+
+ public string ContentType
+ {
+ get { return _contentType; }
+
+ set { _contentType = value; }
+ }
+ private string _contentType;
+
+ public bool HeadersSent
+ {
+ get { return _headersSent; }
+ }
+ private bool _headersSent;
+
+ public bool Sent
+ {
+ get { return _sent; }
+ }
+ private bool _sent;
+
+ public ResponseCookies Cookies
+ {
+ get { return _cookies; }
+ }
+ private ResponseCookies _cookies = null;
+
+ public TestHttpResponse()
+ {
+ _headersSent = false;
+ _sent = false;
+ }
+
+ public void AddHeader(string name, string value) {}
+
+ public void Send()
+ {
+ if (!_headersSent) SendHeaders();
+ if (_sent) throw new InvalidOperationException("stuff already sent");
+ _sent = true;
+ }
+
+ public void SendBody(byte[] buffer, int offset, int count)
+ {
+ if (!_headersSent) SendHeaders();
+ _sent = true;
+ }
+
+ public void SendBody(byte[] buffer)
+ {
+ if (!_headersSent) SendHeaders();
+ _sent = true;
+ }
+
+ public void SendHeaders()
+ {
+ if (_headersSent) throw new InvalidOperationException("headers already sent");
+ _headersSent = true;
+ }
+
+ public void Redirect(Uri uri) {}
+ public void Redirect(string url) {}
+ }
+}
\ No newline at end of file
--
cgit v1.1