diff --git a/src/BloomExe/BloomExe.csproj b/src/BloomExe/BloomExe.csproj
index b2cef97d42ea..0752bab9d8d4 100644
--- a/src/BloomExe/BloomExe.csproj
+++ b/src/BloomExe/BloomExe.csproj
@@ -185,6 +185,7 @@ copy /Y "$(SolutionDir)\lib\dotnet\libtidy.dll" $(OutDir)
+
@@ -351,4 +352,4 @@ copy /Y "$(SolutionDir)\lib\dotnet\libtidy.dll" $(OutDir)
-
\ No newline at end of file
+
diff --git a/src/BloomExe/web/BloomHttpListenerContext.cs b/src/BloomExe/web/BloomHttpListenerContext.cs
index 95fef614dd57..fa8a661b991d 100644
--- a/src/BloomExe/web/BloomHttpListenerContext.cs
+++ b/src/BloomExe/web/BloomHttpListenerContext.cs
@@ -1,24 +1,26 @@
-using System.Net;
+using System.IO;
+using System.Net;
+using EmbedIO;
namespace Bloom.web
{
///
/// At this point, the only point of these classes is so we can write tests without having
- /// to try to spin up a real HttpListener which causes problems on TeamCity.
+ /// to try to spin up a real HTTP server which causes problems on TeamCity.
///
- /// The Bloom... concrete classes are simply wrappers for the real objects.
+ /// The Bloom... concrete classes are wrappers for EmbedIO's IHttpContext.
///
public interface IHttpListenerContext
{
IHttpListenerRequest Request { get; }
- HttpListenerResponse Response { get; }
+ IHttpListenerResponse Response { get; }
}
public class BloomHttpListenerContext : IHttpListenerContext
{
- private readonly HttpListenerContext _actualContext;
+ private readonly IHttpContext _actualContext;
- public BloomHttpListenerContext(HttpListenerContext context)
+ public BloomHttpListenerContext(IHttpContext context)
{
_actualContext = context;
}
@@ -28,9 +30,9 @@ public IHttpListenerRequest Request
get { return new BloomHttpListenerRequest(_actualContext.Request); }
}
- public HttpListenerResponse Response
+ public IHttpListenerResponse Response
{
- get { return _actualContext.Response; }
+ get { return new BloomHttpListenerResponse(_actualContext.Response); }
}
}
@@ -47,9 +49,9 @@ public interface IHttpListenerRequest
public class BloomHttpListenerRequest : IHttpListenerRequest
{
- private readonly HttpListenerRequest _actualRequest;
+ private readonly IHttpRequest _actualRequest;
- public BloomHttpListenerRequest(HttpListenerRequest request)
+ public BloomHttpListenerRequest(IHttpRequest request)
{
_actualRequest = request;
}
@@ -71,7 +73,7 @@ public bool HasEntityBody
public string HttpMethod
{
- get { return _actualRequest.HttpMethod; }
+ get { return _actualRequest.HttpVerb.ToString(); }
}
public System.IO.Stream InputStream
@@ -89,4 +91,79 @@ public System.Uri Url
get { return _actualRequest.Url; }
}
}
+
+ public interface IHttpListenerResponse
+ {
+ string ContentType { get; set; }
+ long ContentLength64 { get; set; }
+ int StatusCode { get; set; }
+ string StatusDescription { get; set; }
+ Stream OutputStream { get; }
+ void AppendHeader(string name, string value);
+ void Close(byte[] buffer, bool willBlock);
+ void Close();
+ }
+
+ public class BloomHttpListenerResponse : IHttpListenerResponse
+ {
+ private readonly IHttpResponse _actualResponse;
+ private long _contentLength;
+
+ public BloomHttpListenerResponse(IHttpResponse response)
+ {
+ _actualResponse = response;
+ }
+
+ public string ContentType
+ {
+ get { return _actualResponse.ContentType; }
+ set { _actualResponse.ContentType = value; }
+ }
+
+ public long ContentLength64
+ {
+ get { return _contentLength; }
+ set
+ {
+ _contentLength = value;
+ _actualResponse.ContentLength64 = value;
+ }
+ }
+
+ public int StatusCode
+ {
+ get { return _actualResponse.StatusCode; }
+ set { _actualResponse.StatusCode = value; }
+ }
+
+ public string StatusDescription
+ {
+ get { return _actualResponse.StatusDescription; }
+ set { _actualResponse.StatusDescription = value; }
+ }
+
+ public Stream OutputStream
+ {
+ get { return _actualResponse.OutputStream; }
+ }
+
+ public void AppendHeader(string name, string value)
+ {
+ _actualResponse.Headers.Add(name, value);
+ }
+
+ public void Close(byte[] buffer, bool willBlock)
+ {
+ if (buffer != null && buffer.Length > 0)
+ {
+ _actualResponse.OutputStream.Write(buffer, 0, buffer.Length);
+ }
+ _actualResponse.Close();
+ }
+
+ public void Close()
+ {
+ _actualResponse.Close();
+ }
+ }
}
diff --git a/src/BloomExe/web/BloomServer.cs b/src/BloomExe/web/BloomServer.cs
index e16ab366cac0..f1d43cf78dc3 100644
--- a/src/BloomExe/web/BloomServer.cs
+++ b/src/BloomExe/web/BloomServer.cs
@@ -28,6 +28,9 @@
using Bloom.web;
using Bloom.web.controllers;
using DesktopAnalytics;
+using EmbedIO;
+using EmbedIO.Routing;
+using EmbedIO.WebApi;
using L10NSharp;
using SIL.Code;
using SIL.IO;
@@ -81,14 +84,14 @@ public static string ServerUrlWithBloomPrefixEndingInSlash
}
///
- /// Listens for requests"
+ /// EmbedIO web server that listens for requests
///
- private HttpListener _listener;
+ private WebServer _webServer;
///
- /// Requests that come into the _listener are placed in the _queue so they can be processed
+ /// Requests that come into the server are placed in the _queue so they can be processed
///
- private readonly Queue _queue;
+ private readonly ConcurrentQueue _queue;
// tasks that should be postponed until no server actions are happening.
@@ -186,7 +189,7 @@ public BloomServer(
BloomFileLocator fileLocator = null
)
{
- _queue = new Queue();
+ _queue = new ConcurrentQueue();
_stop = new ManualResetEvent(false);
_ready = new ManualResetEvent(false);
_listenerThread = new Thread(EnqueueIncomingRequests);
@@ -1192,7 +1195,7 @@ private static bool IsSimulatedFileUrl(string localPath)
///
///
///
- protected bool IsRecursiveRequestContext(HttpListenerContext context)
+ protected bool IsRecursiveRequestContext(IHttpContext context)
{
return context.Request.QueryString["generateThumbnailIfNecessary"] == "true";
}
@@ -1287,7 +1290,7 @@ private bool ProcessCssFile(IRequestInfo info, string incomingPath)
///
public virtual void EnsureListening()
{
- if (_listener?.IsListening != true)
+ if (_webServer == null || _webServer.State != WebServerState.Listening)
StartListening();
}
@@ -1344,12 +1347,10 @@ private bool AttemptToOpenPort()
Logger.WriteMinorEvent(
"Attempting to start http listener on " + ServerUrlEndingInSlash
);
- _listener = new HttpListener
- {
- AuthenticationSchemes = AuthenticationSchemes.Anonymous
- };
- _listener.Prefixes.Add(ServerUrlEndingInSlash);
- _listener.Start();
+ _webServer = new WebServer(
+ o => o.WithUrlPrefix(ServerUrlEndingInSlash).WithMode(HttpListenerMode.EmbedIO)
+ ).WithModule(new BloomServerModule("/", this));
+ _webServer.Start();
return true;
}
catch (HttpListenerException error)
@@ -1385,10 +1386,9 @@ private bool HandleExceptionOpeningPort(Exception error)
);
try
{
- if (_listener != null)
+ if (_webServer != null)
{
- //_listener.Stop(); this will always throw if we failed to start, so skip it and go to the close:
- _listener.Close();
+ _webServer.Dispose();
}
}
catch (Exception)
@@ -1397,11 +1397,30 @@ private bool HandleExceptionOpeningPort(Exception error)
}
finally
{
- _listener = null;
+ _webServer = null;
}
return false;
}
+ ///
+ /// Called by BloomServerModule to handle incoming requests
+ ///
+ public async Task HandleRequestAsync(IHttpContext context)
+ {
+ // Use a TaskCompletionSource to bridge between EmbedIO's async model and our queue-based threading model
+ var tcs = new TaskCompletionSource();
+
+ // Store the TCS with the context so we can signal completion later
+ context.Items["CompletionSource"] = tcs;
+
+ // Add request to queue
+ _queue.Enqueue(context);
+ _ready.Set();
+
+ // Wait for processing to complete
+ await tcs.Task;
+ }
+
public static bool ServerIsListening { get; internal set; }
private static void VerifyWeAreNowListening()
@@ -1498,11 +1517,12 @@ private void SpinUpAWorker()
#endregion
///
- /// The _listenerThread runs this method, and exits when the _stop event is raised
+ /// The _listenerThread runs this method, and exits when the _stop event is raised.
+ /// With EmbedIO, this just monitors for dead worker threads.
///
private void EnqueueIncomingRequests()
{
- while (_listener.IsListening)
+ while (!_stop.WaitOne(1000))
{
// We've found that sometimes one of our worker threads just dies. One way to force it to happen is to
// uncomment the block of code that converts requests for .map files into 404s.
@@ -1527,36 +1547,29 @@ private void EnqueueIncomingRequests()
_workers.TryRemove(kvp.Key, out Thread _);
// Seems like just making one would be enough, but preliminary testing still found the number
// declining slowly.
- while (_workers.Count < MinWorkerThreads)
- SpinUpAWorker();
+ lock (_queue)
+ {
+ while (_workers.Count < MinWorkerThreads)
+ SpinUpAWorker();
+ }
}
-
- var context = _listener.BeginGetContext(QueueRequest, null);
-
- if (0 == WaitHandle.WaitAny(new[] { _stop, context.AsyncWaitHandle }))
- return;
}
}
///
- /// This method is called in the _listenerThread when we obtain an HTTP request from
- /// the _listener, and queues it for processing by a worker.
+ /// No longer needed with EmbedIO - requests are queued directly by HandleRequestAsync.
+ /// This method is kept for backward compatibility but does nothing.
///
///
private void QueueRequest(IAsyncResult ar)
{
- // this can happen when shutting down
- // BL-2207 indicates it may be possible for the thread to be alive and the listener closed,
- // although the only way I know it gets closed happens after joining with that thread.
- // Still, one more check seems worthwhile...if we're far enough along in shutting down
- // to have closed the listener we certainly can't respond to any more requests.
- if (!_listenerThread.IsAlive || !_listener.IsListening)
- return;
+ // No longer used with EmbedIO
+ }
+ private void CheckForBlockedWorkers()
+ {
lock (_queue)
{
- _queue.Enqueue(_listener.EndGetContext(ar));
-
// Deal with a situation where all the workers are blocked,
// but there is a request in the queue that would unblock the current workers
// but that request can't run because it's stuck in queue
@@ -1588,20 +1601,18 @@ private void RequestProcessorLoop()
// _ready in the wait array) if a request is ready, and 1 when _stop is signaled, breaking us out of the loop.
while (WaitHandle.WaitAny(wait) == 0)
{
- HttpListenerContext context;
- bool isRecursiveRequestContext; // needs to be declared outside the lock but initialized afte we have the context.
- lock (_queue)
+ IHttpContext context;
+ bool isRecursiveRequestContext;
+
+ // Try to dequeue a request
+ if (!_queue.TryDequeue(out context))
{
- if (_queue.Count > 0)
- {
- context = _queue.Dequeue();
- }
- else
- {
- _ready.Reset();
- continue;
- }
+ _ready.Reset();
+ continue;
+ }
+ lock (_queue)
+ {
isRecursiveRequestContext = IsRecursiveRequestContext(context);
if (isRecursiveRequestContext)
{
@@ -1618,8 +1629,15 @@ private void RequestProcessorLoop()
}
var rawurl = "unknown";
+ TaskCompletionSource tcs = null;
try
{
+ // Get the TaskCompletionSource if it exists
+ if (context.Items.TryGetValue("CompletionSource", out var tcsObj))
+ {
+ tcs = tcsObj as TaskCompletionSource;
+ }
+
rawurl = context.Request.RawUrl;
// Enhance: the DAISY ACE accessibility report points at images in the epub, correctly and raw, like "tiger.png"
@@ -1636,9 +1654,8 @@ private void RequestProcessorLoop()
)
{
var r = new RequestInfo(new BloomHttpListenerContext(context));
-
r.WriteError(404);
-
+ tcs?.SetResult(true);
return;
}
// Uncommenting this is a way to cause lots of worker threads to die when an inspector is opened.
@@ -1657,6 +1674,9 @@ private void RequestProcessorLoop()
Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
MakeReply(new RequestInfo(new BloomHttpListenerContext(context)));
+
+ // Signal completion
+ tcs?.SetResult(true);
}
catch (HttpListenerException e)
{
@@ -1666,6 +1686,7 @@ private void RequestProcessorLoop()
+ e.Message
);
Logger.WriteEvent("At BloomServer: ListenerCallback(): url=" + rawurl);
+ tcs?.SetException(e);
}
catch (Exception error)
{
@@ -1700,6 +1721,7 @@ error is IOException
Debug.Fail("(Debug Only) " + error.Message);
#endif
}
+ tcs?.SetException(error);
}
finally
{
@@ -2155,7 +2177,7 @@ protected virtual void Dispose(bool fDisposing)
try
{
ServerIsListening = false;
- if (_listener != null)
+ if (_webServer != null)
{
//prompted by the mysterious BL 273, Crash while closing down the imageserver
Guard.AgainstNull(_listenerThread, "_listenerThread");
@@ -2198,19 +2220,9 @@ var kvp in _workers.Where(
}
}
- // stop listening for incoming http requests
- Debug.Assert(_listener.IsListening);
- if (_listener.IsListening)
- {
- //In BL-3290, a user quitely failed here each time he exited Bloom, with a Cannot access a disposed object.
- //according to http://stackoverflow.com/questions/11164919/why-httplistener-start-method-dispose-stuff-on-exception,
- //it's actually just responding to being closed, not disposed.
- //I don't know *why* for that user the listener was already stopped.
- _listener.Stop();
- }
- //if we keep getting that exception, we could move the Close() into the previous block
- _listener.Close();
- _listener = null;
+ // stop the EmbedIO web server
+ _webServer.Dispose();
+ _webServer = null;
}
if (_cache != null)
{
diff --git a/src/BloomExe/web/BloomServerModule.cs b/src/BloomExe/web/BloomServerModule.cs
new file mode 100644
index 000000000000..379df5e82bd1
--- /dev/null
+++ b/src/BloomExe/web/BloomServerModule.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2014-2018 SIL International
+// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)
+using System;
+using System.Threading.Tasks;
+using EmbedIO;
+
+namespace Bloom.Api
+{
+ ///
+ /// Custom EmbedIO module that forwards all requests to BloomServer's queue-based processing system
+ ///
+ public class BloomServerModule : WebModuleBase
+ {
+ private readonly BloomServer _bloomServer;
+
+ public BloomServerModule(string baseRoute, BloomServer bloomServer)
+ : base(baseRoute)
+ {
+ _bloomServer = bloomServer;
+ }
+
+ public override bool IsFinalHandler => true;
+
+ protected override async Task OnRequestAsync(IHttpContext context)
+ {
+ // Queue the request for processing by BloomServer's worker threads
+ await _bloomServer.HandleRequestAsync(context);
+ }
+ }
+}
diff --git a/src/BloomExe/web/RequestInfo.cs b/src/BloomExe/web/RequestInfo.cs
index d1bd732137e1..99fcbf472e79 100644
--- a/src/BloomExe/web/RequestInfo.cs
+++ b/src/BloomExe/web/RequestInfo.cs
@@ -86,7 +86,7 @@ public void WriteCompleteOutput(string s)
WriteOutput(Encoding.UTF8.GetBytes(s), _actualContext.Response);
}
- private void WriteOutput(byte[] buffer, HttpListenerResponse response)
+ private void WriteOutput(byte[] buffer, IHttpListenerResponse response)
{
response.ContentLength64 += buffer.Length;
// This is particularly useful in allowing the bloom-player used in the BloomPUB preview
@@ -105,6 +105,13 @@ private void WriteOutput(byte[] buffer, HttpListenerResponse response)
{
ReportHttpListenerProblem(e);
}
+ catch (IOException e)
+ {
+ // EmbedIO may throw IOException when the connection is closed prematurely
+ Logger.WriteEvent(
+ "Could not write requested data: connection closed. " + e.Message
+ );
+ }
HaveOutput = true;
}
diff --git a/src/BloomTests/web/RequestInfoTests.cs b/src/BloomTests/web/RequestInfoTests.cs
index 82fb26d72d6b..bc211bbccd14 100644
--- a/src/BloomTests/web/RequestInfoTests.cs
+++ b/src/BloomTests/web/RequestInfoTests.cs
@@ -88,7 +88,7 @@ private TempFile MakeTempFile(byte[] contents)
private class TestHttpListenerContext : IHttpListenerContext
{
public IHttpListenerRequest Request { get; private set; }
- public HttpListenerResponse Response { get; private set; }
+ public IHttpListenerResponse Response { get; private set; }
public void SetRequest(IHttpListenerRequest request)
{