diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index b48d4eb5e9..2a149baa76 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -1,18 +1,14 @@ using System; using System.Configuration; using System.Threading; -using System.Web; -using System.Web.Caching; -using Umbraco.Core.Cache; +using System.Threading.Tasks; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Profiling; using Umbraco.Core.Services; using Umbraco.Core.Sync; - namespace Umbraco.Core { /// @@ -80,13 +76,13 @@ namespace Umbraco.Core /// public static ApplicationContext EnsureContext(ApplicationContext appContext, bool replaceContext) { - if (ApplicationContext.Current != null) + if (Current != null) { if (!replaceContext) - return ApplicationContext.Current; + return Current; } - ApplicationContext.Current = appContext; - return ApplicationContext.Current; + Current = appContext; + return Current; } /// @@ -106,14 +102,14 @@ namespace Umbraco.Core [Obsolete("Use the other method specifying an ProfilingLogger instead")] public static ApplicationContext EnsureContext(DatabaseContext dbContext, ServiceContext serviceContext, CacheHelper cache, bool replaceContext) { - if (ApplicationContext.Current != null) + if (Current != null) { if (!replaceContext) - return ApplicationContext.Current; + return Current; } var ctx = new ApplicationContext(dbContext, serviceContext, cache); - ApplicationContext.Current = ctx; - return ApplicationContext.Current; + Current = ctx; + return Current; } /// @@ -242,9 +238,10 @@ namespace Umbraco.Core /// - http://issues.umbraco.org/issue/U4-5728 /// - http://issues.umbraco.org/issue/U4-5391 /// - internal string UmbracoApplicationUrl { - get - { + internal string UmbracoApplicationUrl + { + get + { // if initialized, return if (_umbracoApplicationUrl != null) return _umbracoApplicationUrl; @@ -259,13 +256,18 @@ namespace Umbraco.Core _umbracoApplicationUrl = value; } } + /// internal string _umbracoApplicationUrl; // internal for tests - private Lazy _configured; + private Lazy _configured; + internal MainDom MainDom { get; private set; } + private void Init() - { - + { + MainDom = new MainDom(); + MainDom.Acquire(); + //Create the lazy value to resolve whether or not the application is 'configured' _configured = new Lazy(() => { @@ -304,7 +306,7 @@ namespace Umbraco.Core } }); - } + } private string ConfigurationStatus { @@ -321,12 +323,6 @@ namespace Umbraco.Core } } - private void AssertIsReady() - { - if (!this.IsReady) - throw new Exception("ApplicationContext is not ready yet."); - } - private void AssertIsNotReady() { if (this.IsReady) diff --git a/src/Umbraco.Core/ApplicationEventHandler.cs b/src/Umbraco.Core/ApplicationEventHandler.cs index a725a08a8e..8d97baac95 100644 --- a/src/Umbraco.Core/ApplicationEventHandler.cs +++ b/src/Umbraco.Core/ApplicationEventHandler.cs @@ -74,7 +74,7 @@ namespace Umbraco.Core /// private bool ShouldExecute(ApplicationContext applicationContext) { - if (applicationContext.IsConfigured && applicationContext.DatabaseContext.CanConnect) + if (applicationContext.IsConfigured && applicationContext.DatabaseContext.IsDatabaseConfigured) { return true; } @@ -84,7 +84,7 @@ namespace Umbraco.Core return true; } - if (!applicationContext.DatabaseContext.CanConnect && ExecuteWhenDatabaseNotConfigured) + if (!applicationContext.DatabaseContext.IsDatabaseConfigured && ExecuteWhenDatabaseNotConfigured) { return true; } diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index 608b19a700..b4e8e64312 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -70,7 +70,7 @@ namespace Umbraco.Core { var wait = _semaphore != null ? _semaphore.WaitAsync() - : WaitOneAsync(_semaphore2); + : _semaphore2.WaitOneAsync(); return wait.IsCompleted ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named @@ -83,7 +83,7 @@ namespace Umbraco.Core { var wait = _semaphore != null ? _semaphore.WaitAsync(millisecondsTimeout) - : WaitOneAsync(_semaphore2, millisecondsTimeout); + : _semaphore2.WaitOneAsync(millisecondsTimeout); return wait.IsCompleted ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named @@ -181,40 +181,5 @@ namespace Umbraco.Core Dispose(false); } } - - // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 - // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html - // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... - // version below should be OK - - private static Task WaitOneAsync(WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) - { - var tcs = new TaskCompletionSource(); - var callbackHandleInitLock = new object(); - lock (callbackHandleInitLock) - { - RegisteredWaitHandle callbackHandle = null; - // ReSharper disable once RedundantAssignment - callbackHandle = ThreadPool.RegisterWaitForSingleObject( - handle, - (state, timedOut) => - { - tcs.SetResult(null); - - // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. - lock (callbackHandleInitLock) - { - // ReSharper disable once PossibleNullReferenceException - // ReSharper disable once AccessToModifiedClosure - callbackHandle.Unregister(null); - } - }, - /*state:*/ null, - /*millisecondsTimeOutInterval:*/ millisecondsTimeout, - /*executeOnlyOnce:*/ true); - } - - return tcs.Task; - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index a4a6938fc0..ca1f4e85a0 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -102,7 +102,7 @@ namespace Umbraco.Core.Cache // cannot create value within the lock, so if result.IsValueCreated is false, just // do nothing here - means that if creation throws, a race condition could cause // more than one thread to reach the return statement below and throw - accepted. - + if (result == null || GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null { result = GetSafeLazy(getCacheItem); @@ -122,7 +122,7 @@ namespace Umbraco.Core.Cache if (eh != null) throw eh.Exception; // throw once! return value; } - + #endregion #region Insert diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index 88fee8ef3a..e7f5d17b83 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -35,7 +35,7 @@ namespace Umbraco.Core.Cache { const string prefix = CacheItemPrefix + "-"; return _cache.Cast() - .Where(x => x.Key is string && ((string) x.Key).StartsWith(prefix)); + .Where(x => x.Key is string && ((string)x.Key).StartsWith(prefix)); } protected override void RemoveEntry(string key) @@ -191,7 +191,7 @@ namespace Umbraco.Core.Cache var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache if (value == null) return; // do not store null values (backward compat) - cacheKey = GetCacheKey(cacheKey); + cacheKey = GetCacheKey(cacheKey); var absolute = isSliding ? System.Web.Caching.Cache.NoAbsoluteExpiration : (timeout == null ? System.Web.Caching.Cache.NoAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index 76519a6722..8afa5639f1 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core.Cache { if (MemoryCache[key] == null) return; MemoryCache.Remove(key); - } + } } public virtual void ClearCacheObjectTypes(string typeName) @@ -137,7 +137,7 @@ namespace Umbraco.Core.Cache .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); - } + } } public virtual void ClearCacheByKeyExpression(string regexString) @@ -149,7 +149,7 @@ namespace Umbraco.Core.Cache .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); - } + } } #endregion @@ -201,7 +201,7 @@ namespace Umbraco.Core.Cache return GetCacheItem(cacheKey, getCacheItem, null); } - public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal,CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) + public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // see notes in HttpRuntimeCacheProvider @@ -264,7 +264,7 @@ namespace Umbraco.Core.Cache { policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); } - + if (removedCallback != null) { policy.RemovedCallback = arguments => @@ -295,6 +295,5 @@ namespace Umbraco.Core.Cache } return policy; } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 998bda32f2..b51fa1e3b0 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Web; using AutoMapper; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Mapping; @@ -77,7 +79,7 @@ namespace Umbraco.Core _profilingLogger = new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler); _timer = _profilingLogger.TraceDuration( - string.Format("Umbraco application ({0}) starting", UmbracoVersion.GetSemanticVersion().ToSemanticString()), + string.Format("Umbraco {0} application starting on {1}", UmbracoVersion.GetSemanticVersion().ToSemanticString(), NetworkHelper.MachineName), "Umbraco application startup complete"); CreateApplicationCache(); @@ -121,9 +123,25 @@ namespace Umbraco.Core InitializeModelMappers(); - //now we need to call the initialize methods - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationInitialized(UmbracoApplication, ApplicationContext)); + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationInitialized", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationInitialized")) + { + //now we need to call the initialize methods + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationInitialized for handler " + x.GetType(), ex); + throw; + } + }); + } _isInitialized = true; @@ -242,11 +260,27 @@ namespace Umbraco.Core if (_isStarted) throw new InvalidOperationException("The boot manager has already been initialized"); - //call OnApplicationStarting of each application events handler - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarting(UmbracoApplication, ApplicationContext)); + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationStarting", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationStarting")) + { + //call OnApplicationStarting of each application events handler + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationStarting for handler " + x.GetType(), ex); + throw; + } + }); + } - if (afterStartup != null) + if (afterStartup != null) { afterStartup(ApplicationContext.Current); } @@ -268,9 +302,28 @@ namespace Umbraco.Core FreezeResolution(); - //call OnApplicationStarting of each application events handler - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarted(UmbracoApplication, ApplicationContext)); + //Here we need to make sure the db can be connected to + EnsureDatabaseConnection(); + + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationStarted", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationStarted")) + { + //call OnApplicationStarting of each application events handler + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationStarted for handler " + x.GetType(), ex); + throw; + } + }); + } //Now, startup all of our legacy startup handler ApplicationEventsResolver.Current.InstantiateLegacyStartupHandlers(); @@ -288,6 +341,32 @@ namespace Umbraco.Core //stop the timer and log the output _timer.Dispose(); return this; + } + + /// + /// We cannot continue if the db cannot be connected to + /// + private void EnsureDatabaseConnection() + { + if (ApplicationContext.IsConfigured == false) return; + if (ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) return; + + var currentTry = 0; + while (currentTry < 5) + { + if (ApplicationContext.DatabaseContext.CanConnect) + break; + + //wait and retry + Thread.Sleep(1000); + currentTry++; + } + + if (currentTry == 5) + { + throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but the Umbraco cannot connect to the database."); + } + } /// diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index f3665ae0b8..7f8cbbf0f7 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -30,8 +30,6 @@ namespace Umbraco.Core private readonly ILogger _logger; private readonly SqlSyntaxProviders _syntaxProviders; private bool _configured; - private bool _canConnect; - private volatile bool _connectCheck = false; private readonly object _locker = new object(); private string _connectionString; private string _providerName; @@ -113,21 +111,9 @@ namespace Umbraco.Core get { if (IsDatabaseConfigured == false) return false; - - //double check lock so that it is only checked once and is fast - if (_connectCheck == false) - { - lock (_locker) - { - if (_canConnect == false) - { - _canConnect = DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DatabaseProvider); - _connectCheck = true; - } - } - } - - return _canConnect; + var canConnect = DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DatabaseProvider); + LogHelper.Info("CanConnect = " + canConnect); + return canConnect; } } diff --git a/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs b/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs new file mode 100644 index 0000000000..d27d38de9a --- /dev/null +++ b/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + /// + /// An exception that is thrown if the umbraco application cannnot boot + /// + public class UmbracoStartupFailedException : Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UmbracoStartupFailedException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs new file mode 100644 index 0000000000..ca0ecb33fe --- /dev/null +++ b/src/Umbraco.Core/MainDom.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Hosting; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Core +{ + // represents the main domain + class MainDom : IRegisteredObject + { + #region Vars + + // our own lock for local consistency + private readonly object _locko = new object(); + + // async lock representing the main domain lock + private readonly AsyncLock _asyncLock; + private IDisposable _asyncLocker; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + + // indicates whether... + private volatile bool _isMainDom; // we are the main domain + private volatile bool _signaled; // we have been signaled + + // actions to run before releasing the main domain + private readonly SortedList _callbacks = new SortedList(); + + private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds + + #endregion + + #region Ctor + + // initializes a new instance of MainDom + public MainDom() + { + var appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty); + + var lockName = "UMBRACO-" + appId + "-MAINDOM-LCK"; + _asyncLock = new AsyncLock(lockName); + + var eventName = "UMBRACO-" + appId + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + } + + #endregion + + // register a main domain consumer + public bool Register(Action release, int weight = 100) + { + return Register(null, release, weight); + } + + // register a main domain consumer + public bool Register(Action install, Action release, int weight = 100) + { + lock (_locko) + { + if (_signaled) return false; + if (install != null) + install(); + if (release != null) + _callbacks.Add(weight, release); + return true; + } + } + + // handles the signal requesting that the main domain is released + private void OnSignal(string source) + { + // once signaled, we stop waiting, but then there is the hosting environment + // so we have to make sure that we only enter that method once + + lock (_locko) + { + LogHelper.Debug("Signaled" + (_signaled ? " (again)" : "") + " (" + source + ")."); + if (_signaled) return; + if (_isMainDom == false) return; // probably not needed + _signaled = true; + } + + try + { + LogHelper.Debug("Stopping..."); + foreach (var callback in _callbacks.Values) + { + try + { + callback(); // no timeout on callbacks + } + catch (Exception e) + { + LogHelper.Error("Error while running callback, remaining callbacks will not run.", e); + throw; + } + + } + LogHelper.Debug("Stopped."); + } + finally + { + // in any case... + _isMainDom = false; + _asyncLocker.Dispose(); + LogHelper.Debug("Released MainDom."); + } + } + + // acquires the main domain + public bool Acquire() + { + lock (_locko) // we don't want the hosting environment to interfere by signaling + { + // if signaled, too late to acquire, give up + // the handler is not installed so that would be the hosting environment + if (_signaled) + { + LogHelper.Debug("Cannot acquire MainDom (signaled)."); + return false; + } + + LogHelper.Debug("Acquiring MainDom..."); + + // signal other instances that we want the lock, then wait one the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // resetted (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + _asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds); + _isMainDom = true; + + // we need to reset the event, because otherwise we would end up + // signaling ourselves and commiting suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + + _signal.Reset(); + _signal.WaitOneAsync() + .ContinueWith(_ => OnSignal("signal")); + + HostingEnvironment.RegisterObject(this); + + LogHelper.Debug("Acquired MainDom."); + return true; + } + } + + // gets a value indicating whether we are the main domain + public bool IsMainDom + { + get { return _isMainDom; } + } + + // IRegisteredObject + public void Stop(bool immediate) + { + try + { + OnSignal("environment"); // will run once + } + finally + { + HostingEnvironment.UnregisterObject(this); + } + } + } +} diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index d621f18a3c..6323227893 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -137,7 +137,7 @@ namespace Umbraco.Core } return _allAssemblies; - } + } } /// @@ -226,7 +226,7 @@ namespace Umbraco.Core } return LocalFilteredAssemblyCache; - } + } } /// @@ -451,7 +451,7 @@ namespace Umbraco.Core var allTypes = GetTypesWithFormattedException(a) .ToArray(); - var attributedTypes = new Type[] {}; + var attributedTypes = new Type[] { }; try { //now filter the types based on the onlyConcreteClasses flag, not interfaces, not static classes but have @@ -480,7 +480,8 @@ namespace Umbraco.Core //now we need to include types that may be inheriting from sub classes of the attribute type being searched for //so we will search in assemblies that reference those types too. - foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)){ + foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)) + { //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. @@ -610,7 +611,7 @@ namespace Umbraco.Core catch (TypeLoadException ex) { LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", a), ex); - continue; + continue; } //add the types to our list to return @@ -618,7 +619,7 @@ namespace Umbraco.Core { foundAssignableTypes.Add(t); } - + //now we need to include types that may be inheriting from sub classes of the type being searched for //so we will search in assemblies that reference those types too. foreach (var subTypesInAssembly in allSubTypes.GroupBy(x => x.Assembly)) @@ -699,6 +700,10 @@ namespace Umbraco.Core #endregion + //TODO: This isn't very elegant, and will have issues since the AppDomain.CurrentDomain + // doesn't actualy load in all assemblies, only the types that have been referenced so far. + // However, in a web context, the BuildManager will have executed which will force all assemblies + // to be loaded so it's fine for now. public static Type GetTypeByName(string typeName) { var type = Type.GetType(typeName); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e73ff5bd15..045877d52f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -327,6 +327,7 @@ + @@ -334,6 +335,7 @@ + @@ -1327,6 +1329,7 @@ + diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index 33c3d58689..7418d4e5b2 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -45,6 +45,19 @@ namespace Umbraco.Core Logger.Error(typeof(UmbracoApplicationBase), msg, exception); }; + //take care of unhandled exceptions - there is nothing we can do to + // prevent the entire w3wp process to go down but at least we can try + // and log the exception + AppDomain.CurrentDomain.UnhandledException += (_, args) => + { + var exception = (Exception) args.ExceptionObject; + var isTerminating = args.IsTerminating; // always true? + + var msg = "Unhandled exception in AppDomain"; + if (isTerminating) msg += " (terminating)"; + LogHelper.Error(msg, exception); + }; + //boot up the application GetBootManager() .Initialize() @@ -82,7 +95,18 @@ namespace Umbraco.Core protected virtual void OnApplicationStarting(object sender, EventArgs e) { if (ApplicationStarting != null) - ApplicationStarting(sender, e); + { + try + { + ApplicationStarting(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationStarting event handler", ex); + throw; + } + } + } /// @@ -93,7 +117,17 @@ namespace Umbraco.Core protected virtual void OnApplicationStarted(object sender, EventArgs e) { if (ApplicationStarted != null) - ApplicationStarted(sender, e); + { + try + { + ApplicationStarted(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationStarted event handler", ex); + throw; + } + } } /// @@ -104,7 +138,17 @@ namespace Umbraco.Core private void OnApplicationInit(object sender, EventArgs e) { if (ApplicationInit != null) - ApplicationInit(sender, e); + { + try + { + ApplicationInit(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationInit event handler", ex); + throw; + } + } } /// diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs new file mode 100644 index 0000000000..0d840a2496 --- /dev/null +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core +{ + internal static class WaitHandleExtensions + { + + // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 + // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html + // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... + // version below should be OK + + public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) + { + var tcs = new TaskCompletionSource(); + var callbackHandleInitLock = new object(); + lock (callbackHandleInitLock) + { + RegisteredWaitHandle callbackHandle = null; + // ReSharper disable once RedundantAssignment + callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + tcs.SetResult(null); + + // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. + lock (callbackHandleInitLock) + { + // ReSharper disable once PossibleNullReferenceException + // ReSharper disable once AccessToModifiedClosure + callbackHandle.Unregister(null); + } + }, + /*state:*/ null, + /*millisecondsTimeOutInterval:*/ millisecondsTimeout, + /*executeOnlyOnce:*/ true); + } + + return tcs.Task; + } + } +} diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index e6fc492350..812f5ed5e4 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -828,14 +828,6 @@ namespace Umbraco.Tests.Scheduling } public override bool RunsOnShutdown { get { return false; } } - - protected override void Dispose(bool disposing) - { - Disposed = true; - base.Dispose(disposing); - } - - public bool Disposed { get; private set; } } private class MyTask : BaseTask diff --git a/src/Umbraco.Web.UI.Client/src/loader.js b/src/Umbraco.Web.UI.Client/src/loader.js index 9cbb8f233d..dda23dce0a 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.js +++ b/src/Umbraco.Web.UI.Client/src/loader.js @@ -21,8 +21,8 @@ LazyLoad.js( 'lib/jquery-file-upload/jquery.fileupload-angular.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', - 'lib/umbraco/Extensions.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', + 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', 'lib/umbraco/LegacyUmbClientMgr.js', diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 1fa210a19f..6d3afd442d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -146,9 +146,24 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + if ($scope.hasDatetimePickerValue) { + if ($scope.model.config.pickTime) { + $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD HH:mm:ss"); + } + else { + $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD"); + } + } + else { + $scope.model.value = null; + } + }); + //unbind doc click event! $scope.$on('$destroy', function () { $(document).unbind("click", $scope.hidePicker); + unsubscribe(); }); } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 48c7692dae..c552fb6a3b 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -10,6 +10,7 @@ using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using System.Linq; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Logging; using Umbraco.Core.Publishing; using Content = Umbraco.Core.Models.Content; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; @@ -23,7 +24,9 @@ namespace Umbraco.Web.Cache public class CacheRefresherEventHandler : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { + { + LogHelper.Info("Initializing Umbraco internal event handlers for cache refreshing"); + //bind to application tree events ApplicationTreeService.Deleted += ApplicationTreeDeleted; ApplicationTreeService.Updated += ApplicationTreeUpdated; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index ef84f31473..3fa11a0dc2 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down - public override bool RunsOnShutdown { get { return true; } } + public override bool RunsOnShutdown { get { return _timer != null; } } // initialize the first instance, which is inactive (not touched yet) public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, ProfilingLogger logger) @@ -142,13 +142,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache lock (_locko) { _logger.Logger.Debug("Timer: release."); - if (_timer != null) - _timer.Dispose(); - _timer = null; _released = true; - // if running (because of shutdown) this will have no effect - // else it tells the runner it is time to run the task Release(); } } @@ -197,5 +192,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache _content.SaveXmlToFile(); } } + + protected override void DisposeResources() + { + base.DisposeResources(); + + // stop the timer + if (_timer == null) return; + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 388012930b..afa5eb8132 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -96,7 +96,8 @@ namespace Umbraco.Web.Scheduling _logPrefix = "[" + name + "] "; _logger = logger; - HostingEnvironment.RegisterObject(this); + if (options.Hosted) + HostingEnvironment.RegisterObject(this); if (options.AutoStart) StartUp(); diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs index 4688ff37d6..55df42d3b7 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -15,6 +15,8 @@ namespace Umbraco.Web.Scheduling LongRunning = false; KeepAlive = false; AutoStart = false; + PreserveRunningTask = false; + Hosted = true; } /// @@ -36,9 +38,16 @@ namespace Umbraco.Web.Scheduling public bool AutoStart { get; set; } /// - /// Gets or setes a value indicating whether the running task should be preserved + /// Gets or sets a value indicating whether the running task should be preserved /// once completed, or reset to null. For unit tests. /// public bool PreserveRunningTask { get; set; } + + /// + /// Gets or sets a value indicating whether the runner should register with (and be + /// stopped by) the hosting. Otherwise, something else should take care of stopping + /// the runner. True by default. + /// + public bool Hosted { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index d5ffbc811d..d11bf2af35 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -29,11 +29,18 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... - string umbracoAppUrl = null; - - try + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) { - using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + + using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) + { + string umbracoAppUrl = null; + + try { umbracoAppUrl = _appContext.UmbracoApplicationUrl; if (umbracoAppUrl.IsNullOrWhiteSpace()) @@ -54,10 +61,10 @@ namespace Umbraco.Web.Scheduling var result = await wc.SendAsync(request, token); } } - } - catch (Exception e) - { - LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + catch (Exception e) + { + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + } } return true; // repeat diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs index c024382ee8..88fe3d66eb 100644 --- a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -63,7 +63,9 @@ namespace Umbraco.Web.Scheduling protected virtual void Dispose(bool disposing) { - // lock on _latch instead of creating a new object as _timer is + if (disposing == false) return; + + // lock on _latch instead of creating a new object as _latch is // private, non-null, readonly - so safe here lock (_latch) { @@ -71,7 +73,13 @@ namespace Umbraco.Web.Scheduling _disposed = true; _latch.Dispose(); + DisposeResources(); } } + + protected virtual void DisposeResources() + { } + + public bool Disposed { get { return _disposed; } } } } diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index f920b5bb9d..fed261cf6f 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - // maximum age, in minutes private static int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { var maximumAge = 24 * 60; // 24 hours, in minutes @@ -66,6 +65,13 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, server status comes from config and will NOT change } + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 3fea70a2b8..567f85f1f5 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -84,19 +84,13 @@ namespace Umbraco.Web.Scheduling /// and returning a value indicating whether to repeat the task. public abstract Task PerformRunAsync(CancellationToken token); - protected override void Dispose(bool disposing) + protected override void DisposeResources() { - // lock on _timer instead of creating a new object as _timer is - // private, non-null, readonly - so safe here - lock (_timer) - { - if (_disposed) return; - _disposed = true; + base.DisposeResources(); - // stop the timer - _timer.Change(Timeout.Infinite, Timeout.Infinite); - _timer.Dispose(); - } + // stop the timer + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 78a91f6341..11cffb3648 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -38,6 +38,13 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, server status comes from config and will NOT change } + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) { string umbracoAppUrl = null; diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 92214e5199..def7b606b5 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -101,6 +101,13 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, server status comes from config and will NOT change } + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { try diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 22d6b5b54b..d7d331d887 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -81,9 +81,9 @@ namespace Umbraco.Web.Templates //set the doc that was found by id contentRequest.PublishedContent = doc; //set the template, either based on the AltTemplate found or the standard template of the doc - contentRequest.TemplateModel = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates || AltTemplate.HasValue == false - ? ApplicationContext.Current.Services.FileService.GetTemplate(doc.TemplateId) - : ApplicationContext.Current.Services.FileService.GetTemplate(AltTemplate.Value); + contentRequest.TemplateModel = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates || AltTemplate.HasValue == false + ? _umbracoContext.Application.Services.FileService.GetTemplate(doc.TemplateId) + : _umbracoContext.Application.Services.FileService.GetTemplate(AltTemplate.Value); //if there is not template then exit if (!contentRequest.HasTemplate) @@ -103,14 +103,20 @@ namespace Umbraco.Web.Templates //after this page has rendered. SaveExistingItems(); - //set the new items on context objects for this templates execution - SetNewItemsOnContextObjects(contentRequest); + try + { + //set the new items on context objects for this templates execution + SetNewItemsOnContextObjects(contentRequest); - //Render the template - ExecuteTemplateRendering(writer, contentRequest); - - //restore items on context objects to continuing rendering the parent template - RestoreItems(); + //Render the template + ExecuteTemplateRendering(writer, contentRequest); + } + finally + { + //restore items on context objects to continuing rendering the parent template + RestoreItems(); + } + } private void ExecuteTemplateRendering(TextWriter sw, PublishedContentRequest contentRequest) diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 51e19b388a..f021d9c841 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -20,8 +20,8 @@ 'lib/jquery-file-upload/jquery.fileupload-angular.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', - 'lib/umbraco/Extensions.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', + 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', 'lib/umbraco/LegacyUmbClientMgr.js', diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ecb21ba5b7..f32fa34fb5 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,31 +1,30 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; -using System.Web.Hosting; using System.Xml; +using umbraco.BusinessLogic; +using umbraco.cms.businesslogic; +using umbraco.cms.businesslogic.web; +using umbraco.DataLayer; +using umbraco.presentation.nodeFactory; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.web; using Umbraco.Core.Models; using Umbraco.Core.Profiling; -using umbraco.DataLayer; -using umbraco.presentation.nodeFactory; using Umbraco.Web; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Scheduling; -using Node = umbraco.NodeFactory.Node; using File = System.IO.File; +using Node = umbraco.NodeFactory.Node; +using Task = System.Threading.Tasks.Task; namespace umbraco { @@ -36,63 +35,49 @@ namespace umbraco { private XmlCacheFilePersister _persisterTask; + private volatile bool _released; + #region Constructors private content() { if (SyncToXmlFile) { - // if we write to file, prepare the lock - // (if we don't use the file, or just read from it, no need to lock) - InitializeFileLock(); - - // and prepare the persister task - var logger = LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(); - var profingLogger = new ProfilingLogger( - logger, - ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger)); - + var logger = LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(); + var profingLogger = new ProfilingLogger( + logger, + ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger)); + + // prepare the persister task // there's always be one task keeping a ref to the runner // so it's safe to just create it as a local var here var runner = new BackgroundTaskRunner("XmlCacheFilePersister", new BackgroundTaskRunnerOptions { LongRunning = true, - KeepAlive = true - }, logger); - - // when the runner is terminating we need to ensure that no modifications - // to content are possible anymore, as they would not be written out to - // the xml file - unfortunately that is not possible in 7.x because we - // cannot lock the content service... and so we do nothing... - //runner.Terminating += (sender, args) => - //{ - //}; - - // when the runner has terminated we know we will not be writing to the file - // anymore, so we can release the lock now - no need to wait for the AppDomain - // unload - which means any "last minute" saves will be lost - but waiting for - // the AppDomain to unload has issues... - runner.Terminated += (sender, args) => - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - LogHelper.Debug("Release file lock."); - _fileLocked.Dispose(); - _fileLocked = null; - _fileLock = null; // ensure we don't lock again - } - }; + KeepAlive = true, + Hosted = false // main domain will take care of stopping the runner (see below) + }, logger); // create (and add to runner) - _persisterTask = new XmlCacheFilePersister(runner, this, profingLogger); + _persisterTask = new XmlCacheFilePersister(runner, this, profingLogger); + + var registered = ApplicationContext.Current.MainDom.Register( + null, + () => + { + // once released, the cache still works but does not write to file anymore, + // which is OK with database server messenger but will cause data loss with + // another messenger... + + runner.Shutdown(false, true); // wait until flushed + _released = true; + }); + + // failed to become the main domain, we will never use the file + if (registered == false) + runner.Shutdown(false, true); + + _released = (registered == false); } // initialize content - populate the cache @@ -212,7 +197,7 @@ namespace umbraco var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); var attr = ((XmlElement)node).GetAttributeNode("sortOrder"); attr.Value = d.sortOrder.ToString(); - AddOrUpdateXmlNode(xmlContentCopy, d.Id, d.Level, parentId, node); + xmlContentCopy = AddOrUpdateXmlNode(xmlContentCopy, d.Id, d.Level, parentId, node); // update sitemapprovider if (updateSitemapProvider && SiteMap.Provider is UmbracoSiteMapProvider) @@ -376,7 +361,7 @@ namespace umbraco { foreach (Document d in Documents) { - PublishNodeDo(d, safeXml.Xml, true); + safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); } } @@ -398,7 +383,18 @@ namespace umbraco public virtual void ClearDocumentCache(int documentId) { // Get the document - var d = new Document(documentId); + Document d; + try + { + d = new Document(documentId); + } + catch + { + // if we need the document to remove it... this cannot be LB?! + // shortcut everything here + ClearDocumentXmlCache(documentId); + return; + } ClearDocumentCache(d); } @@ -419,26 +415,8 @@ namespace umbraco // remove from xml db cache doc.XmlRemoveFromDB(); - // We need to lock content cache here, because we cannot allow other threads - // making changes at the same time, they need to be queued - using (var safeXml = GetSafeXmlReader()) - { - // Check if node present, before cloning - x = safeXml.Xml.GetElementById(doc.Id.ToString()); - if (x == null) - return; - - safeXml.UpgradeToWriter(false); - - // Find the document in the xml cache - x = safeXml.Xml.GetElementById(doc.Id.ToString()); - if (x != null) - { - // The document already exists in cache, so repopulate it - x.ParentNode.RemoveChild(x); - safeXml.Commit(); - } - } + // clear xml cache + ClearDocumentXmlCache(doc.Id); ClearContextCache(); @@ -450,6 +428,30 @@ namespace umbraco { var prov = (UmbracoSiteMapProvider)SiteMap.Provider; prov.RemoveNode(doc.Id); + } + } + } + + internal void ClearDocumentXmlCache(int id) + { + // We need to lock content cache here, because we cannot allow other threads + // making changes at the same time, they need to be queued + using (var safeXml = GetSafeXmlReader()) + { + // Check if node present, before cloning + var x = safeXml.Xml.GetElementById(id.ToString()); + if (x == null) + return; + + safeXml.UpgradeToWriter(false); + + // Find the document in the xml cache + x = safeXml.Xml.GetElementById(id.ToString()); + if (x != null) + { + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + safeXml.Commit(); } } } @@ -809,7 +811,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return xmlDoc == null ? null : (XmlDocument)xmlDoc.CloneNode(true); } - private static void EnsureSchema(string contentTypeAlias, XmlDocument xml) + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -822,15 +824,22 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // ensure it contains the content type if (subset != null && subset.Contains(string.Format("", contentTypeAlias))) - return; + return xml; - // remove current doctype - xml.RemoveChild(n); + // alas, that does not work, replacing a doctype is ignored and GetElementById fails + // + //// remove current doctype, set new doctype + //xml.RemoveChild(n); + //subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); + //var doctype = xml.CreateDocumentType("root", null, null, subset); + //xml.InsertAfter(doctype, xml.FirstChild); - // set new doctype + var xml2 = new XmlDocument(); subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); - var doctype = xml.CreateDocumentType("root", null, null, subset); - xml.InsertAfter(doctype, xml.FirstChild); + var doctype = xml2.CreateDocumentType("root", null, null, subset); + xml2.AppendChild(doctype); + xml2.AppendChild(xml2.ImportNode(xml.DocumentElement, true)); + return xml2; } private static void InitializeXml(XmlDocument xml, string dtd) @@ -985,100 +994,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; private readonly string _xmlFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); private DateTime _lastFileRead; // last time the file was read private DateTime _nextFileCheck; // last time we checked whether the file was changed - private AsyncLock _fileLock; // protects the file - private IDisposable _fileLocked; // protects the file - - private const int FileLockTimeoutMilliseconds = 4 * 60 * 1000; // 4' - - private void InitializeFileLock() - { - // initialize file lock - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - // - // From MSDN C++ CreateSemaphore doc: - // "The name can have a "Global\" or "Local\" prefix to explicitly create the object in - // the global or session namespace. The remainder of the name can contain any character - // except the backslash character (\). For more information, see Kernel Object Namespaces." - // - // From MSDN "Kernel object namespaces" doc: - // "The separate client session namespaces enable multiple clients to run the same - // applications without interfering with each other. For processes started under - // a client session, the system uses the session namespace by default. However, these - // processes can use the global namespace by prepending the "Global\" prefix to the object name." - // - // just use "default" (whatever it is) for now - ie, no prefix - // - var name = HostingEnvironment.ApplicationID + "/XmlStore/XmlFile"; - _fileLock = new AsyncLock(name); - - // the file lock works with a shared, system-wide, semaphore - and we don't want - // to leak a count on that semaphore else the whole process will hang - so we have - // to ensure we dispose of the locker when the domain goes down - in theory the - // async lock should do it via its finalizer, but then there are some weird cases - // where the semaphore has been disposed of before it's been released, and then - // we'd need to GC-pin the semaphore... better dispose the locker explicitely - // when the app domain unloads. - - if (AppDomain.CurrentDomain.IsDefaultAppDomain()) - { - LogHelper.Debug("Registering Unload handler for default app domain."); - AppDomain.CurrentDomain.ProcessExit += OnDomainUnloadReleaseFileLock; - } - else - { - LogHelper.Debug("Registering Unload handler for non-default app domain."); - AppDomain.CurrentDomain.DomainUnload += OnDomainUnloadReleaseFileLock; - } - } - - private void EnsureFileLock() - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked != null) return; // locked already - - // thread-safety, acquire lock only once! - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLock == null) return; - if (_fileLocked != null) return; - - // don't hang forever, throws if it cannot lock within the timeout - LogHelper.Debug("Acquiring exclusive access to file for this AppDomain..."); - _fileLocked = _fileLock.Lock(FileLockTimeoutMilliseconds); - LogHelper.Debug("Acquired exclusive access to file for this AppDomain."); - } - } - - private void OnDomainUnloadReleaseFileLock(object sender, EventArgs args) - { - // the unload event triggers AFTER all hosted objects (eg the file persister - // background task runner) have been stopped, so we should have released the - // lock already - this is for safety - might be possible to get rid of it - - // NOTE - // trying to write to the log via LogHelper at that point is a BAD idea - // it can lead to ugly deadlocks with the named semaphore - DONT do it - - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - // in case you really need to debug... that should be safe... - //System.IO.File.AppendAllText(HostingEnvironment.MapPath("~/App_Data/log.txt"), string.Format("{0} {1} unlock", DateTime.Now, AppDomain.CurrentDomain.Id)); - _fileLocked.Dispose(); - - _fileLock = null; // ensure we don't lock again - } - } // not used - just try to read the file //private bool XmlFileExists @@ -1100,7 +1015,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) internal void SaveXmlToFile() { LogHelper.Info("Save Xml to file..."); @@ -1110,8 +1027,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1119,7 +1034,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1140,8 +1055,10 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock - internal async System.Threading.Tasks.Task SaveXmlToFileAsync() + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) + internal async Task SaveXmlToFileAsync() { LogHelper.Info("Save Xml to file..."); @@ -1150,8 +1067,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1159,7 +1074,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1208,17 +1123,15 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return sb.ToString(); } - // assumes file lock private XmlDocument LoadXmlFromFile() { + // do NOT try to load if we are not the main domain anymore + if (_released) return null; + LogHelper.Info("Load Xml from file..."); try { - // if we're not writing back to the file, no need to lock - if (SyncToXmlFile) - EnsureFileLock(); - var xml = new XmlDocument(); using (var fs = new FileStream(_xmlFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { @@ -1241,12 +1154,11 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // (files is always locked) private void DeleteXmlFile() { - if (System.IO.File.Exists(_xmlFileName) == false) return; - System.IO.File.SetAttributes(_xmlFileName, FileAttributes.Normal); - System.IO.File.Delete(_xmlFileName); + if (File.Exists(_xmlFileName) == false) return; + File.SetAttributes(_xmlFileName, FileAttributes.Normal); + File.Delete(_xmlFileName); } private void ReloadXmlFromFileIfChanged() @@ -1277,7 +1189,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; #region Manage change // adds or updates a node (docNode) into a cache (xml) - public static void AddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) + public static XmlDocument AddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) { // sanity checks if (id != docNode.AttributeValue("id")) @@ -1291,7 +1203,12 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // if the document is not there already then it's a new document // we must make sure that its document type exists in the schema if (currentNode == null && UseLegacySchema == false) - EnsureSchema(docNode.Name, xml); + { + var xml2 = EnsureSchema(docNode.Name, xml); + if (ReferenceEquals(xml, xml2) == false) + docNode = xml2.ImportNode(docNode, true); + xml = xml2; + } // find the parent XmlNode parentNode = level == 1 @@ -1300,7 +1217,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // no parent = cannot do anything if (parentNode == null) - return; + return xml; // insert/move the node under the parent if (currentNode == null) @@ -1368,6 +1285,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // then we just need to ensure that currentNode is at the right position. // should be faster that moving all the nodes around. XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + return xml; } private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode documentNode, XmlNode publishedNode) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 8e6e952abe..c0f470e91f 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -102,7 +102,7 @@ namespace umbraco.presentation.preview if (document.ContentEntity.Published == false && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - content.AddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); + XmlContent = content.AddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); } if (includeSubs) @@ -112,7 +112,7 @@ namespace umbraco.presentation.preview var previewXml = XmlContent.ReadNode(XmlReader.Create(new StringReader(prevNode.Xml))); if (prevNode.IsDraft) previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - content.AddOrUpdateXmlNode(XmlContent, prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml); + XmlContent = content.AddOrUpdateXmlNode(XmlContent, prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml); } } diff --git a/src/UmbracoExamine/Config/IndexSetExtensions.cs b/src/UmbracoExamine/Config/IndexSetExtensions.cs index ac432b16ec..1255f50a3c 100644 --- a/src/UmbracoExamine/Config/IndexSetExtensions.cs +++ b/src/UmbracoExamine/Config/IndexSetExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Examine; @@ -17,60 +16,7 @@ namespace UmbracoExamine.Config internal static IIndexCriteria ToIndexCriteria(this IndexSet set, IDataService svc, IEnumerable indexFieldPolicies) { - - var attributeFields = set.IndexAttributeFields.Cast().ToArray(); - var userFields = set.IndexUserFields.Cast().ToArray(); - var includeNodeTypes = set.IncludeNodeTypes.ToList().Select(x => x.Name).ToArray(); - var excludeNodeTypes = set.ExcludeNodeTypes.ToList().Select(x => x.Name).ToArray(); - var parentId = set.IndexParentId; - - //if there are no user fields defined, we'll populate them from the data source (include them all) - if (set.IndexUserFields.Count == 0) - { - //we need to add all user fields to the collection if it is empty (this is the default if none are specified) - var userProps = svc.ContentService.GetAllUserPropertyNames(); - var fields = new List(); - foreach (var u in userProps) - { - var field = new IndexField() { Name = u }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == u); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } - fields.Add(field); - } - userFields = fields.ToArray(); - } - - //if there are no attribute fields defined, we'll populate them from the data source (include them all) - if (set.IndexAttributeFields.Count == 0) - { - //we need to add all system fields to the collection if it is empty (this is the default if none are specified) - var sysProps = svc.ContentService.GetAllSystemPropertyNames(); - var fields = new List(); - foreach (var s in sysProps) - { - var field = new IndexField() { Name = s }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == s); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } - fields.Add(field); - } - attributeFields = fields.ToArray(); - } - - - return new IndexCriteria( - attributeFields, - userFields, - includeNodeTypes, - excludeNodeTypes, - parentId); + return new LazyIndexCriteria(set, svc, indexFieldPolicies); } /// diff --git a/src/UmbracoExamine/Config/LazyIndexCriteria.cs b/src/UmbracoExamine/Config/LazyIndexCriteria.cs new file mode 100644 index 0000000000..72ab3f31ba --- /dev/null +++ b/src/UmbracoExamine/Config/LazyIndexCriteria.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Config; +using UmbracoExamine.DataServices; + +namespace UmbracoExamine.Config +{ + internal class LazyIndexCriteria : IIndexCriteria + { + public LazyIndexCriteria( + IndexSet set, + IDataService svc, + IEnumerable indexFieldPolicies) + { + if (set == null) throw new ArgumentNullException("set"); + if (indexFieldPolicies == null) throw new ArgumentNullException("indexFieldPolicies"); + if (svc == null) throw new ArgumentNullException("svc"); + + _lazyCriteria = new Lazy(() => + { + var attributeFields = set.IndexAttributeFields.Cast().ToArray(); + var userFields = set.IndexUserFields.Cast().ToArray(); + var includeNodeTypes = set.IncludeNodeTypes.Cast().Select(x => x.Name).ToArray(); + var excludeNodeTypes = set.ExcludeNodeTypes.Cast().Select(x => x.Name).ToArray(); + var parentId = set.IndexParentId; + + //if there are no user fields defined, we'll populate them from the data source (include them all) + if (set.IndexUserFields.Count == 0) + { + //we need to add all user fields to the collection if it is empty (this is the default if none are specified) + var userProps = svc.ContentService.GetAllUserPropertyNames(); + var fields = new List(); + foreach (var u in userProps) + { + var field = new IndexField() { Name = u }; + var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == u); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } + fields.Add(field); + } + userFields = fields.ToArray(); + } + + //if there are no attribute fields defined, we'll populate them from the data source (include them all) + if (set.IndexAttributeFields.Count == 0) + { + //we need to add all system fields to the collection if it is empty (this is the default if none are specified) + var sysProps = svc.ContentService.GetAllSystemPropertyNames(); + var fields = new List(); + foreach (var s in sysProps) + { + var field = new IndexField() { Name = s }; + var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == s); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } + fields.Add(field); + } + attributeFields = fields.ToArray(); + } + + + return new IndexCriteria( + attributeFields, + userFields, + includeNodeTypes, + excludeNodeTypes, + parentId); + }); + } + + private readonly Lazy _lazyCriteria; + + public IEnumerable ExcludeNodeTypes + { + get { return _lazyCriteria.Value.ExcludeNodeTypes; } + } + + public IEnumerable IncludeNodeTypes + { + get { return _lazyCriteria.Value.IncludeNodeTypes; } + } + + public int? ParentNodeId + { + get { return _lazyCriteria.Value.ParentNodeId; } + } + + public IEnumerable StandardFields + { + get { return _lazyCriteria.Value.StandardFields; } + } + + public IEnumerable UserFields + { + get { return _lazyCriteria.Value.UserFields; } + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index b7fc35542a..b913c2bdf2 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -111,6 +111,7 @@ +