diff --git a/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs b/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs index d402adc42..963f6efde 100644 --- a/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs +++ b/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs @@ -4,7 +4,7 @@ using Orchard.Environment.Warmup; namespace Orchard.Tests.Environment.Warmup { [TestFixture] - public class WarmUpUtilityTests { + public class WarmupUtilityTests { [Test] public void EmptyStringsAreNotAllowed() { diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index cc6d269b4..b11d043f2 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -240,7 +240,7 @@ - + diff --git a/src/Orchard.Web/Global.asax.cs b/src/Orchard.Web/Global.asax.cs index c9d4fb80d..3a6c20533 100644 --- a/src/Orchard.Web/Global.asax.cs +++ b/src/Orchard.Web/Global.asax.cs @@ -1,22 +1,18 @@ using System; -using System.IO; using System.Threading; using System.Web; -using System.Web.Hosting; using System.Web.Mvc; using System.Web.Routing; using Autofac; using Orchard.Environment; -using Orchard.Environment.Warmup; -using Orchard.Utility.Extensions; namespace Orchard.Web { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : HttpApplication { - private static StartupResult _startupResult; - private static EventWaitHandle _waitHandle; + private static IOrchardHost _host; + private static Exception _error; public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); @@ -26,77 +22,30 @@ namespace Orchard.Web { LaunchStartupThread(); } - /// - /// Initializes Orchard's Host in a separate thread - /// - private static void LaunchStartupThread() { - _startupResult = new StartupResult(); - _waitHandle = new AutoResetEvent(false); - - ThreadPool.QueueUserWorkItem( - state => { - try { - RegisterRoutes(RouteTable.Routes); - var host = OrchardStarter.CreateHost(MvcSingletons); - host.Initialize(); - _startupResult.Host = host; - } - catch (Exception e) { - _startupResult.Error = e; - } - finally { - _waitHandle.Set(); - } - }); - } - protected void Application_BeginRequest() { - // Host is still starting up? - if (_startupResult.Host == null && _startupResult.Error == null) { + if (_error != null) { + // Host startup resulted in an error - // use the url as it was requested by the client - // the real url might be different if it has been translated (proxy, load balancing, ...) - var url = Request.ToUrlString(); - var virtualFileCopy = "~/App_Data/WarmUp/" + WarmupUtility.EncodeUrl(url.Trim('/')); - var localCopy = HostingEnvironment.MapPath(virtualFileCopy); - - if (File.Exists(localCopy)) { - // result should not be cached, even on proxies - Context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); - Context.Response.Cache.SetValidUntilExpires(false); - Context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); - Context.Response.Cache.SetCacheability(HttpCacheability.NoCache); - Context.Response.Cache.SetNoStore(); - - Context.Response.WriteFile(localCopy); - Context.Response.End(); - } - else if(!File.Exists(Request.PhysicalPath)) { - // there is no local copy and the host is not running - // wait for the host to initialize - _waitHandle.WaitOne(); - } + // Throw error once, and restart launch; machine state may have changed + // so we need to simulate a "restart". + var error = _error; + LaunchStartupThread(); + throw error; } - else { - if (_startupResult.Error != null) { - // Host startup resulted in an error - // Throw error once, and restart launch (machine state may have changed - // so we need to simulate a "restart". - var error = _startupResult.Error; - LaunchStartupThread(); - throw error; - } - - Context.Items["originalHttpContext"] = Context; - _startupResult.Host.BeginRequest(); + // Only notify if the host has started up + if (_host == null) { + return; } + + Context.Items["originalHttpContext"] = Context; + _host.BeginRequest(); } protected void Application_EndRequest() { // Only notify if the host has started up - if (_startupResult.Host != null) { - _startupResult.Host.EndRequest(); + if (_host != null) { + _host.EndRequest(); } } @@ -105,5 +54,31 @@ namespace Orchard.Web { builder.Register(ctx => ModelBinders.Binders).SingleInstance(); builder.Register(ctx => ViewEngines.Engines).SingleInstance(); } + + /// + /// Initializes Orchard's Host in a separate thread + /// + private static void LaunchStartupThread() { + RegisterRoutes(RouteTable.Routes); + + _host = null; + _error = null; + + ThreadPool.QueueUserWorkItem( + state => { + try { + var host = OrchardStarter.CreateHost(MvcSingletons); + host.Initialize(); + _host = host; + } + catch (Exception e) { + _error = e; + } + finally { + // Execute pending actions as the host is available + WarmupHttpModule.Signal(); + } + }); + } } } diff --git a/src/Orchard.Web/Orchard.Web.csproj b/src/Orchard.Web/Orchard.Web.csproj index db45cf2f6..783a5c721 100644 --- a/src/Orchard.Web/Orchard.Web.csproj +++ b/src/Orchard.Web/Orchard.Web.csproj @@ -127,6 +127,7 @@ Global.asax + Designer diff --git a/src/Orchard.Web/WarmupHttpModule.cs b/src/Orchard.Web/WarmupHttpModule.cs new file mode 100644 index 000000000..7f4acac6d --- /dev/null +++ b/src/Orchard.Web/WarmupHttpModule.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Web; +using System.Web.Hosting; + +namespace Orchard.Web { + public class WarmupHttpModule : IHttpModule { + private const string WarmupFilesPath = "~/App_Data/Warmup/"; + private HttpApplication _context; + + public void Init(HttpApplication context) { + _context = context; + context.AddOnBeginRequestAsync(BeginBeginRequest, EndBeginRequest, null); + } + + static IList _awaiting = new List(); + + public static bool InWarmup() { + if (_awaiting == null) return false; + lock (_awaiting) { + return _awaiting != null; + } + } + + public static void Signal() { + lock(typeof(WarmupHttpModule)) { + var awaiting = _awaiting; + _awaiting = null; + foreach (var action in awaiting) { + action(); + } + } + } + + public static void Await(Action action) { + if (_awaiting == null) { + action(); + return; + } + + lock(typeof(WarmupHttpModule)) { + if (_awaiting == null) { + action(); + return; + } + _awaiting.Add(action); + } + } + + private IAsyncResult BeginBeginRequest(object sender, EventArgs e, AsyncCallback cb, object extradata) { + var asyncResult = new WarmupAsyncResult(cb); + + // host is available, process every requests, or file is processed + if (!InWarmup() || DoBeginRequest()) { + asyncResult.Done(); + } + else { + // this is the "on hold" execution path + Await(asyncResult.Done); + } + + return asyncResult; + } + + private void EndBeginRequest(IAsyncResult ar) { + ((WarmupAsyncResult)ar).Wait(); + } + + public void Dispose() { + } + + /// + /// return true to put request on hold (until call to Signal()) - return false to allow pipeline to execute immediately + /// + /// + private bool DoBeginRequest() { + // use the url as it was requested by the client + // the real url might be different if it has been translated (proxy, load balancing, ...) + var url = ToUrlString(_context.Request); + var virtualFileCopy = WarmupFilesPath + EncodeUrl(url.Trim('/')); + var localCopy = HostingEnvironment.MapPath(virtualFileCopy); + + if (localCopy != null && File.Exists(localCopy)) { + // result should not be cached, even on proxies + _context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); + _context.Response.Cache.SetValidUntilExpires(false); + _context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + _context.Response.Cache.SetCacheability(HttpCacheability.NoCache); + _context.Response.Cache.SetNoStore(); + + _context.Response.WriteFile(localCopy); + _context.Response.End(); + return true; + } + + // there is no local copy and the file exists + // serve the static file + if (File.Exists(_context.Request.PhysicalPath)) { + return true; + } + + return false; + } + + public static string EncodeUrl(string url) { + if(String.IsNullOrWhiteSpace(url)) { + throw new ArgumentException("url can't be empty"); + } + + var sb = new StringBuilder(); + foreach(var c in url.ToLowerInvariant()) { + // only accept alphanumeric chars + if((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + sb.Append(c); + } + // otherwise encode them in UTF8 + else { + sb.Append("_"); + foreach(var b in Encoding.UTF8.GetBytes(new [] {c})) { + sb.Append(b.ToString("X")); + } + } + } + + return sb.ToString(); + } + + public static string ToUrlString(HttpRequest request) { + return string.Format("{0}://{1}{2}", request.Url.Scheme, request.Headers["Host"], request.RawUrl); + } + + private class WarmupAsyncResult : IAsyncResult { + readonly EventWaitHandle _eventWaitHandle = new AutoResetEvent(false); + + private readonly AsyncCallback _cb; + + public WarmupAsyncResult(AsyncCallback cb) { + _cb = cb; + IsCompleted = false; + } + + public void Done() { + IsCompleted = true; + _eventWaitHandle.Set(); + _cb(this); + } + + public object AsyncState { + get { return null; } + } + + public WaitHandle AsyncWaitHandle { + get { throw new NotImplementedException(); } + } + + public bool CompletedSynchronously { + get { return true; } + } + + public bool IsCompleted { get; private set; } + + public void Wait() { + _eventWaitHandle.WaitOne(); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index 1dba63ec0..498046275 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -120,6 +120,10 @@ + + + + diff --git a/src/Orchard/Environment/Warmup/StartupResult.cs b/src/Orchard/Environment/Warmup/StartupResult.cs deleted file mode 100644 index 0a1400f90..000000000 --- a/src/Orchard/Environment/Warmup/StartupResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Orchard.Environment.Warmup { - public class StartupResult { - public IOrchardHost Host { get; set; } - public Exception Error { get; set; } - } -} diff --git a/src/Orchard/Environment/Warmup/WarmupUtility.cs b/src/Orchard/Environment/Warmup/WarmupUtility.cs index e13003fd4..b964ef244 100644 --- a/src/Orchard/Environment/Warmup/WarmupUtility.cs +++ b/src/Orchard/Environment/Warmup/WarmupUtility.cs @@ -1,19 +1,29 @@ using System; -using System.Linq; using System.Text; -using System.Text.RegularExpressions; namespace Orchard.Environment.Warmup { public static class WarmupUtility { - private const string EncodingPattern = "[^a-z0-9]"; - public static string EncodeUrl(string url) { if(String.IsNullOrWhiteSpace(url)) { throw new ArgumentException("url can't be empty"); } - return Regex.Replace(url.ToLower(), EncodingPattern, m => "_" + Encoding.UTF8.GetBytes(m.Value).Select(b => b.ToString("X")).Aggregate((a, b) => a + b)); - } + var sb = new StringBuilder(); + foreach (var c in url.ToLowerInvariant()) { + // only accept alphanumeric chars + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + sb.Append(c); + } + // otherwise encode them in UTF8 + else { + sb.Append("_"); + foreach(var b in Encoding.UTF8.GetBytes(new [] {c})) { + sb.Append(b.ToString("X")); + } + } + } + return sb.ToString(); + } } } diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index dc713d2b6..f6e62d379 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -187,7 +187,6 @@ -