From 610582df38c00f36fac5991958fdcf0ca9fd2d36 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:02:30 +0200 Subject: [PATCH 01/45] U4-5162 - do not cache macro if previewing --- src/Umbraco.Web/umbraco.presentation/macro.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 7b9c63e6b8..136c30c799 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -537,8 +537,8 @@ namespace umbraco /// private Control AddMacroResultToCache(Control macroControl) { - // Add result to cache if successful - if (Model.CacheDuration > 0) + // Add result to cache if successful (and cache is enabled) + if (UmbracoContext.Current.InPreviewMode == false && Model.CacheDuration > 0) { // do not add to cache if there's no member and it should cache by personalization if (!Model.CacheByMember || (Model.CacheByMember && Member.IsLoggedOn())) From 69ac621f5af69743372a0c8891e43b2aa08dd1a9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:03:10 +0200 Subject: [PATCH 02/45] Fixes: U4-5670 Umbraco.DropDown List pre value with alias ** already exists for this data type --- .../DataTypeDefinitionRepository.cs | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 2a58879025..b606830c62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -493,15 +493,10 @@ AND umbracoNode.id <> @id", throw new InvalidOperationException("Cannot insert a pre value for a data type that has no identity"); } - //Cannot add a duplicate alias - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataTypePreValues -WHERE alias = @alias -AND datatypeNodeId = @dtdid", - new { alias = entity.Alias, dtdid = entity.DataType.Id }); - if (exists > 0) - { - throw new DuplicateNameException("A pre value with the alias " + entity.Alias + " already exists for this data type"); - } + //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT + // in reality there is no need to check the uniqueness of this alias because the only way that this code executes is + // based on an IDictionary dictionary being passed to this repository and a dictionary + // must have unique aliases by definition, so there is no need for this additional check var dto = new DataTypePreValueDto { @@ -519,17 +514,12 @@ AND datatypeNodeId = @dtdid", { throw new InvalidOperationException("Cannot update a pre value for a data type that has no identity"); } - - //Cannot change to a duplicate alias - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataTypePreValues -WHERE alias = @alias -AND datatypeNodeId = @dtdid -AND id <> @id", - new { id = entity.Id, alias = entity.Alias, dtdid = entity.DataType.Id }); - if (exists > 0) - { - throw new DuplicateNameException("A pre value with the alias " + entity.Alias + " already exists for this data type"); - } + + //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT + // this causes issues when sorting the pre-values (http://issues.umbraco.org/issue/U4-5670) but in reality + // there is no need to check the uniqueness of this alias because the only way that this code executes is + // based on an IDictionary dictionary being passed to this repository and a dictionary + // must have unique aliases by definition, so there is no need for this additional check var dto = new DataTypePreValueDto { From 811ef4a44494f06f00813288efa0f61eff0463a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:03:42 +0200 Subject: [PATCH 03/45] Fixes: U4-5673 SQL Azure cannot drop clustered index on upgrade --- .../AdditionalIndexesAndKeys.cs | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs index 198e8307eb..dc775cb426 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs @@ -42,11 +42,33 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero Create.Index("IX_cmsDocument_newest").OnTable("cmsDocument").OnColumn("newest").Ascending().WithOptions().NonClustered(); } - //TODO: We need to fix this for SQL Azure since it does not let you drop any clustered indexes - // Issue: http://issues.umbraco.org/issue/U4-5673 - // Some work around notes: - // http://stackoverflow.com/questions/15872347/alter-clustered-index-column - // https://social.msdn.microsoft.com/Forums/azure/en-US/5cc4b302-fa42-4c62-956a-bbf79dbbd040/changing-clustered-index-in-azure?forum=ssdsgetstarted + //We need to do this for SQL Azure V2 since it does not let you drop any clustered indexes + // Issue: http://issues.umbraco.org/issue/U4-5673 + if (Context.CurrentDatabaseProvider == DatabaseProviders.SqlServer || Context.CurrentDatabaseProvider == DatabaseProviders.SqlAzure) + { + var version = Context.Database.ExecuteScalar("SELECT @@@@VERSION"); + if (version.Contains("Microsoft SQL Azure")) + { + var parts = version.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + if (parts.Length > 1) + { + if (parts[1].StartsWith("11.")) + { + + //we want to drop the umbracoUserLogins_Index index since it is named incorrectly and then re-create it so + // it follows the standard naming convention + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("umbracoUserLogins_Index"))) + { + //It's the old version that doesn't support dropping a clustered index on a table, so we need to do some manual work. + ExecuteSqlAzureSqlForChangingIndex(); + } + + return; + } + } + } + } + //we want to drop the umbracoUserLogins_Index index since it is named incorrectly and then re-create it so // it follows the standard naming convention @@ -67,5 +89,28 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero Delete.Index("IX_cmsDocument_published").OnTable("cmsDocument"); Delete.Index("IX_cmsDocument_newest").OnTable("cmsDocument"); } + + private void ExecuteSqlAzureSqlForChangingIndex() + { + Context.Database.Execute(@"CREATE TABLE ""umbracoUserLogins_temp"" +( + contextID uniqueidentifier NOT NULL, + userID int NOT NULL, + [timeout] bigint NOT NULL +); +CREATE CLUSTERED INDEX ""IX_umbracoUserLogins_Index"" ON ""umbracoUserLogins_temp"" (""contextID""); +INSERT INTO ""umbracoUserLogins_temp"" SELECT * FROM ""umbracoUserLogins"" +DROP TABLE ""umbracoUserLogins"" +CREATE TABLE ""umbracoUserLogins"" +( + contextID uniqueidentifier NOT NULL, + userID int NOT NULL, + [timeout] bigint NOT NULL +); +CREATE CLUSTERED INDEX ""IX_umbracoUserLogins_Index"" ON ""umbracoUserLogins"" (""contextID""); +INSERT INTO ""umbracoUserLogins"" SELECT * FROM ""umbracoUserLogins_temp"" +DROP TABLE ""umbracoUserLogins_temp"""); + + } } } \ No newline at end of file From a1f4d7b16ffc10fcf5f195485d53fd0154c4fd96 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:05:30 +0200 Subject: [PATCH 04/45] Fixes: U4-5798 The database configuration failed with the following message: The incoming request has too many parameters. The server supports a maximum of 2100 parameters. --- .../TargetVersionSeven/AlterTagRelationsTable.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs index 31cfeb7997..0ad3d8e5b9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs @@ -34,15 +34,20 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven if (Context.CurrentDatabaseProvider == DatabaseProviders.MySql) { Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("nodeId").ToTable("umbracoNode").PrimaryColumn("id"); + //check for another strange really old one that might have existed + if (constraints.Any(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "tagId")) + { + Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("tagId").ToTable("cmsTags").PrimaryColumn("id"); + } } else { //Before we try to delete this constraint, we'll see if it exists first, some older schemas never had it and some older schema's had this named // differently than the default. - var constraint = constraints - .SingleOrDefault(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); - if (constraint != null) + var constraintMatches = constraints.Where(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); + + foreach (var constraint in constraintMatches) { Delete.ForeignKey(constraint.Item3).OnTable("cmsTagRelationship"); } From 3e7c9fc81e4cf3c7f61a63a6ff70c11ccbc69869 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:07:53 +0200 Subject: [PATCH 05/45] Fixes: U4-6202 Cache not clearing by object type properly Conflicts: src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs --- .../Cache/DictionaryCacheProviderBase.cs | 21 ++++++++++++--- .../Cache/HttpRuntimeCacheProvider.cs | 6 +++++ .../Cache/ObjectCacheRuntimeCacheProvider.cs | 27 ++++++++++++++++--- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs index a2bcaee9c9..a6666d4e76 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs @@ -134,7 +134,15 @@ namespace Umbraco.Core.Cache // compare on exact type, don't use "is" // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType() == typeOfT; + + //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 + // until then we will check if 'T' is an interface and if so we will use the 'is' clause, + // otherwise we do an exact match. + + return value == null || + (typeOfT.IsInterface + ? (value is T) + : value.GetType() == typeOfT); }) .ToArray()) RemoveEntry((string) entry.Key); @@ -156,9 +164,14 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - return value.GetType() == typeOfT - // run predicate on the 'public key' part only, ie without prefix - && predicate(((string)x.Key).Substring(plen), (T)value); + + //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 + // until then we will check if 'T' is an interface and if so we will use the 'is' clause, + // otherwise we do an exact match. + + return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT)) + // run predicate on the 'public key' part only, ie without prefix + && predicate(((string) x.Key).Substring(plen), (T) value); })) RemoveEntry((string) entry.Key); } diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index 748912b9f1..dcd5198eec 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -20,9 +20,15 @@ namespace Umbraco.Core.Cache private readonly System.Web.Caching.Cache _cache; + /// + /// Used for debugging + /// + internal Guid InstanceId { get; private set; } + public HttpRuntimeCacheProvider(System.Web.Caching.Cache cache) { _cache = cache; + InstanceId = Guid.NewGuid(); } protected override IEnumerable GetDictionaryEntries() diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index d4f2c33ed1..19fbb0694b 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -16,12 +16,19 @@ namespace Umbraco.Core.Cache /// internal class ObjectCacheRuntimeCacheProvider : IRuntimeCacheProvider { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); internal ObjectCache MemoryCache; + /// + /// Used for debugging + /// + internal Guid InstanceId { get; private set; } + public ObjectCacheRuntimeCacheProvider() { MemoryCache = new MemoryCache("in-memory"); + InstanceId = Guid.NewGuid(); } #region Clear @@ -75,7 +82,16 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType() == typeOfT; + + //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 + // until then we will check if 'T' is an interface and if so we will use the 'is' clause, + // otherwise we do an exact match. + + return value == null || + (typeOfT.IsInterface + ? (value is T) + : value.GetType() == typeOfT); + }) .Select(x => x.Key) .ToArray()) // ToArray required to remove @@ -96,8 +112,13 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - return value.GetType() == typeOfT - && predicate(x.Key, (T) value); + + //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 + // until then we will check if 'T' is an interface and if so we will use the 'is' clause, + // otherwise we do an exact match. + + return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT)) + && predicate(x.Key, (T)value); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove From 04976f8063dc181aad36e4f22c6eaacc2b92ef58 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:08:19 +0200 Subject: [PATCH 06/45] U4-6202 - cache clearing by object type Conflicts: src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs --- .../Cache/DictionaryCacheProviderBase.cs | 29 +++++++------- src/Umbraco.Core/Cache/ICacheProvider.cs | 38 +++++++++++++++++++ .../Cache/ObjectCacheRuntimeCacheProvider.cs | 30 ++++++++------- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs index a6666d4e76..543609dd2e 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs @@ -105,6 +105,9 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(string typeName) { + var type = Type.GetType(typeName); + if (type == null) return; + var isInterface = type.IsInterface; using (WriteLock) { foreach (var entry in GetDictionaryEntries() @@ -114,7 +117,10 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType().ToString().InvariantEquals(typeName); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); }) .ToArray()) RemoveEntry((string) entry.Key); @@ -124,6 +130,7 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes() { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; using (WriteLock) { foreach (var entry in GetDictionaryEntries() @@ -135,14 +142,9 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); - //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 - // until then we will check if 'T' is an interface and if so we will use the 'is' clause, - // otherwise we do an exact match. - - return value == null || - (typeOfT.IsInterface - ? (value is T) - : value.GetType() == typeOfT); + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); }) .ToArray()) RemoveEntry((string) entry.Key); @@ -152,6 +154,7 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(Func predicate) { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; var plen = CacheItemPrefix.Length + 1; using (WriteLock) { @@ -165,11 +168,9 @@ namespace Umbraco.Core.Cache var value = GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 - // until then we will check if 'T' is an interface and if so we will use the 'is' clause, - // otherwise we do an exact match. - - return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT)) + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? (value is T) : (value.GetType() == typeOfT)) // run predicate on the 'public key' part only, ie without prefix && predicate(((string) x.Key).Substring(plen), (T) value); })) diff --git a/src/Umbraco.Core/Cache/ICacheProvider.cs b/src/Umbraco.Core/Cache/ICacheProvider.cs index 6cec619be8..0d2bb1bdb6 100644 --- a/src/Umbraco.Core/Cache/ICacheProvider.cs +++ b/src/Umbraco.Core/Cache/ICacheProvider.cs @@ -8,13 +8,51 @@ namespace Umbraco.Core.Cache /// public interface ICacheProvider { + /// + /// Removes all items from the cache. + /// void ClearAllCache(); + + /// + /// Removes an item from the cache, identified by its key. + /// + /// The key of the item. void ClearCacheItem(string key); + + /// + /// Removes items from the cache, of a specified type. + /// + /// The name of the type to remove. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// Performs a case-sensitive search. + /// void ClearCacheObjectTypes(string typeName); + + /// + /// Removes items from the cache, of a specified type. + /// + /// The type of the items to remove. + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). void ClearCacheObjectTypes(); + + /// + /// Removes items from the cache, of a specified type, satisfying a predicate. + /// + /// The type of the items to remove. + /// The predicate to satisfy. + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). void ClearCacheObjectTypes(Func predicate); + void ClearCacheByKeySearch(string keyStartsWith); void ClearCacheByKeyExpression(string regexString); + IEnumerable GetCacheItemsByKeySearch(string keyStartsWith); IEnumerable GetCacheItemsByKeyExpression(string regexString); diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index 19fbb0694b..197fdce7f7 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Runtime.Caching; using System.Text.RegularExpressions; using System.Threading; @@ -53,6 +54,9 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(string typeName) { + var type = Type.GetType(typeName); + if (type == null) return; + var isInterface = type.IsInterface; using (new WriteLock(_locker)) { foreach (var key in MemoryCache @@ -62,7 +66,10 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType().ToString().InvariantEquals(typeName); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove @@ -75,6 +82,7 @@ namespace Umbraco.Core.Cache using (new WriteLock(_locker)) { var typeOfT = typeof (T); + var isInterface = typeOfT.IsInterface; foreach (var key in MemoryCache .Where(x => { @@ -83,14 +91,9 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); - //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 - // until then we will check if 'T' is an interface and if so we will use the 'is' clause, - // otherwise we do an exact match. - - return value == null || - (typeOfT.IsInterface - ? (value is T) - : value.GetType() == typeOfT); + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); }) .Select(x => x.Key) @@ -104,6 +107,7 @@ namespace Umbraco.Core.Cache using (new WriteLock(_locker)) { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; foreach (var key in MemoryCache .Where(x => { @@ -113,11 +117,9 @@ namespace Umbraco.Core.Cache var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 - // until then we will check if 'T' is an interface and if so we will use the 'is' clause, - // otherwise we do an exact match. - - return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT)) + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? (value is T) : (value.GetType() == typeOfT)) && predicate(x.Key, (T)value); }) .Select(x => x.Key) From d00bf4649dfbad8c1b4901d2cf172b60dee70ca3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:10:18 +0200 Subject: [PATCH 07/45] Fix for JsSource on legacy trees not supporting virtual paths (~/) - U4-6346 Conflicts: src/umbraco.cms/Actions/Action.cs --- .../RelationTypes/TreeMenu/ActionDeleteRelationType.cs | 2 +- .../developer/RelationTypes/TreeMenu/ActionNewRelationType.cs | 2 +- src/umbraco.cms/Actions/Action.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs index c9d7a21dfb..436ead7c93 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs @@ -67,7 +67,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string JsSource { - get { return "/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.js"; } + get { return "~/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.js"; } } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs index 2dcbbfb077..4b36559da2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs @@ -67,7 +67,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string JsSource { - get { return "/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.js"; } + get { return "~/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.js"; } } /// diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index 075cd84436..724ae421da 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -13,6 +13,7 @@ using umbraco.cms.businesslogic.workflow; using umbraco.interfaces; using System.Text.RegularExpressions; using System.Linq; +using Umbraco.Core.IO; using TypeFinder = Umbraco.Core.TypeFinder; namespace umbraco.BusinessLogic.Actions @@ -88,7 +89,7 @@ namespace umbraco.BusinessLogic.Actions { return ActionsResolver.Current.Actions .Where(x => !string.IsNullOrWhiteSpace(x.JsSource)) - .Select(x => x.JsSource).ToList(); + .Select(x => IOHelper.ResolveUrl(x.JsSource)).ToList(); //return ActionJsReference; } From 275ba00e24872c5c3b1c24a813cff89615a27790 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:10:30 +0200 Subject: [PATCH 08/45] Fix IsBackOfficeRequest returning false for virtual directory paths - U4-6346 --- src/Umbraco.Core/UriExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 53441d7ea1..48e9b83b20 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); //check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/')); + var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); //if not, then def not back office if (isUmbracoPath == false) return false; From da7456fd2a3bba4309dffe9e6c056b1101d44376 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:11:17 +0200 Subject: [PATCH 09/45] Fixes up unit test for testing back office request to ensure that the GlobalSettings.Path is consistent with a vdir --- src/Umbraco.Tests/UriExtensionsTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Tests/UriExtensionsTests.cs b/src/Umbraco.Tests/UriExtensionsTests.cs index 1b0ae96621..4ea8e8875a 100644 --- a/src/Umbraco.Tests/UriExtensionsTests.cs +++ b/src/Umbraco.Tests/UriExtensionsTests.cs @@ -4,12 +4,19 @@ using System.Linq; using System.Text; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.IO; namespace Umbraco.Tests { [TestFixture] public class UriExtensionsTests { + [TearDown] + public void TearDown() + { + SystemDirectories.Root = ""; + } + [TestCase("http://www.domain.com/umbraco", "", true)] [TestCase("http://www.domain.com/Umbraco/", "", true)] [TestCase("http://www.domain.com/umbraco/default.aspx", "", true)] @@ -35,6 +42,8 @@ namespace Umbraco.Tests [TestCase("http://www.domain.com/umbraco/test/legacyAjaxCalls.ashx?some=query&blah=js", "", true)] public void Is_Back_Office_Request(string input, string virtualPath, bool expected) { + SystemDirectories.Root = virtualPath; + var source = new Uri(input); Assert.AreEqual(expected, source.IsBackOfficeRequest(virtualPath)); } From 45bec2a67d3c7b2607718b911e8a39874849a8ab Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:12:19 +0200 Subject: [PATCH 10/45] Fixes date picker: U4-6715 i can't seem to remove a "publish-at" date? - date picker is more robust now and does proper validity checking/feedback --- .../src/less/property-editors.less | 2 + .../datepicker/datepicker.controller.js | 87 ++++++++++++++----- .../datepicker/datepicker.html | 34 +++++--- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index f34d45be63..e06a45771b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -522,3 +522,5 @@ ul.color-picker li a { .bootstrap-datetimepicker-widget .btn{padding: 0;} .bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} .umb-datepicker .input-append .add-on{cursor: pointer;} +.umb-datepicker p {margin-top:10px;} +.umb-datepicker p a{color: @gray;} \ No newline at end of file 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 eb49bc3b39..64a22d4c54 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 @@ -18,7 +18,8 @@ function dateTimePickerController($scope, notificationsService, assetsService, a //map the user config $scope.model.config = angular.extend(config, $scope.model.config); - $scope.datetimePickerValue = $scope.model.value; + $scope.hasDatetimePickerValue = $scope.model.value ? true : false; + $scope.datetimePickerValue = null; //hide picker if clicking on the document $scope.hidePicker = function () { @@ -35,14 +36,20 @@ function dateTimePickerController($scope, notificationsService, assetsService, a function applyDate(e) { angularHelper.safeApply($scope, function() { // when a date is changed, update the model - if (e.date) { - if ($scope.model.config.pickTime) { - $scope.model.value = e.date.format("YYYY-MM-DD HH:mm:ss"); + if (e.date && e.date.isValid()) { + $scope.datePickerForm.datepicker.$setValidity("pickerError", true); + $scope.hasDatetimePickerValue = true; + if (!$scope.model.config.format) { + $scope.datetimePickerValue = e.date; } else { - $scope.model.value = e.date.format("YYYY-MM-DD"); + $scope.datetimePickerValue = e.date.format($scope.model.config.format); } } + else { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + } if (!$scope.model.config.pickTime) { $element.find("div:first").datetimepicker("hide", 0); @@ -50,6 +57,15 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); } + var picker = null; + + $scope.clearDate = function() { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + $scope.model.value = null; + $scope.datePickerForm.datepicker.$setValidity("pickerError", true); + } + //get the current user to see if we can localize this picker userService.getCurrentUser().then(function (user) { @@ -69,25 +85,39 @@ function dateTimePickerController($scope, notificationsService, assetsService, a // Get the id of the datepicker button that was clicked var pickerId = $scope.model.alias; - // Open the datepicker and add a changeDate eventlistener - $element.find("div:first") - .datetimepicker($scope.model.config) - .on("dp.change", applyDate); + var element = $element.find("div:first"); - //manually assign the date to the plugin - if (!$scope.model.config.format) { - $element.find("div:first").datetimepicker("setValue", $scope.model.value ? $scope.model.value : null); - } - else { - $element.find("div:first").datetimepicker("setValue", $scope.model.value ? new Date($scope.model.value) : null); - if ($scope.model.value && $scope.model.config.format) { - $scope.datetimePickerValue = moment($scope.model.value).format($scope.model.config.format); - } - } + // Open the datepicker and add a changeDate eventlistener + element + .datetimepicker(angular.extend({ useCurrent: true }, $scope.model.config)) + .on("dp.change", applyDate) + .on("dp.error", function(a, b, c) { + $scope.hasDatetimePickerValue = false; + $scope.datePickerForm.datepicker.$setValidity("pickerError", false); + }); + + if ($scope.hasDatetimePickerValue) { + + //assign value to plugin/picker + element.datetimepicker("setValue", $scope.model.value ? new Date($scope.model.value) : moment()); + + if (!$scope.model.config.format) { + $scope.datetimePickerValue = moment($scope.model.value); + } + else { + $scope.datetimePickerValue = moment($scope.model.value).format($scope.model.config.format); + } + } + + element.find("input").bind("blur", function() { + //we need to force an apply here + $scope.$apply(); + }); //Ensure to remove the event handler when this instance is destroyted - $scope.$on('$destroy', function () { - $element.find("div:first").datetimepicker("destroy"); + $scope.$on('$destroy', function () { + element.find("input").unbind("blur"); + element.datetimepicker("destroy"); }); }); }); @@ -95,9 +125,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.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index b3e503eb4a..898afe88a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -1,14 +1,26 @@
-
- - - - -
+ +
+ + - Required - {{propertyForm.datepicker.errorMsg}} + + + + +
+ + Required + {{datePickerForm.datepicker.errorMsg}} + Invalid date + +

+ Clear date +

+ +
From 2600a3887ed82a9178a3f4cb93b68a8d506355fe Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:13:24 +0200 Subject: [PATCH 11/45] Fixes issue with upgrading - potential file lock issues and also file check for access.config Conflicts: src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs --- .../Install/FilePermissionHelper.cs | 2 +- .../umbraco.presentation/content.cs | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 013f40fc0e..b5bc908fde 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -98,7 +98,7 @@ namespace Umbraco.Web.Install // that and we might get lock issues. try { - var xmlFile = content.Instance.UmbracoXmlDiskCacheFileName + ".tmp"; + var xmlFile = content.GetUmbracoXmlDiskFileName() + ".tmp"; SaveAndDeleteFile(xmlFile); return true; } diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ff08f36207..fd542f0fe8 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -127,23 +127,26 @@ namespace umbraco private static readonly object DbReadSyncLock = new object(); private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; - private string _umbracoXmlDiskCacheFileName = string.Empty; + private static string _umbracoXmlDiskCacheFileName = string.Empty; private volatile XmlDocument _xmlContent; /// /// Gets the path of the umbraco XML disk cache file. /// /// The name of the umbraco XML disk cache file. + public static string GetUmbracoXmlDiskFileName() + { + if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) + { + _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); + } + return _umbracoXmlDiskCacheFileName; + } + + [Obsolete("Use the safer static GetUmbracoXmlDiskFileName() method instead to retrieve this value")] public string UmbracoXmlDiskCacheFileName { - get - { - if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) - { - _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); - } - return _umbracoXmlDiskCacheFileName; - } + get { return GetUmbracoXmlDiskFileName(); } set { _umbracoXmlDiskCacheFileName = value; } } @@ -669,9 +672,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; { //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? - if (File.Exists(UmbracoXmlDiskCacheFileName)) + if (File.Exists(GetUmbracoXmlDiskFileName())) { - return new FileInfo(UmbracoXmlDiskCacheFileName).LastWriteTimeUtc; + return new FileInfo(GetUmbracoXmlDiskFileName()).LastWriteTimeUtc; } return DateTime.MinValue; From 18080c1cfb2fc1844c2c0e617072ecbbb06bf1a9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:13:49 +0200 Subject: [PATCH 12/45] Fixes: U4-6796 BackOfficeIdentity gets assigned inadvertently for front-end requests if using ui.Text to localize --- .../BasePages/BasePage.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 51f059dbb5..8e7f1f47e1 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; +using System.Web.UI; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -181,7 +182,14 @@ namespace umbraco.BasePages /// public static int GetUserId() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity == null) return -1; return Convert.ToInt32(identity.Id); @@ -205,7 +213,14 @@ namespace umbraco.BasePages /// public static bool ValidateCurrentUser() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity != null) { return true; @@ -232,7 +247,14 @@ namespace umbraco.BasePages { get { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + return identity == null ? "" : identity.SessionId; } set From bcb5883230b3fc1c17aff27bd8a2b1e128545e2f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:16:46 +0200 Subject: [PATCH 13/45] Fixes: U4-3514 Add dropdown list of tabs, if more then 6 tabs Conflicts: src/Umbraco.Web.UI.Client/bower.json src/Umbraco.Web.UI.Client/gruntFile.js --- src/Umbraco.Web.UI.Client/bower.json | 53 ++++++++++--------- src/Umbraco.Web.UI.Client/gruntFile.js | 3 ++ .../directives/html/umbheader.directive.js | 2 + src/Umbraco.Web.UI.Client/src/loader.js | 22 ++++---- src/Umbraco.Web/UI/JavaScript/JsInitialize.js | 1 + 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..2c4b4d8d86 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -1,28 +1,29 @@ { - "name": "Umbraco", - "version": "7", - "homepage": "https://github.com/umbraco/Umbraco-CMS", - "authors": [ - "Shannon " - ], - "description": "Umbraco CMS", - "license": "MIT", - "private": true, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "typeahead.js": "~0.10.5", - "underscore": "~1.7.0", - "rgrove-lazyload": "*", - "jquery": "2.0.3", - "jquery-file-upload": "~9.4.0", - "jquery-ui": "1.10.3", - "angular-dynamic-locale": "~0.1.27" + "name": "Umbraco", + "version": "7", + "homepage": "https://github.com/umbraco/Umbraco-CMS", + "authors": [ + "Shannon " + ], + "description": "Umbraco CMS", + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "typeahead.js": "~0.10.5", + "underscore": "~1.7.0", + "rgrove-lazyload": "*", + "jquery": "2.0.3", + "jquery-file-upload": "~9.4.0", + "jquery-ui": "1.10.3", + "angular-dynamic-locale": "~0.1.27", + "bootstrap-tabdrop": "~1.0.0" }, "exportsOverride": { "rgrove-lazyload": { @@ -57,5 +58,5 @@ "blueimp-canvas-to-blob": { "ignore": "*.ignore" } - } -} \ No newline at end of file + } +} diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index ef636d2ada..03b6c6ccbf 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -419,6 +419,9 @@ module.exports = function (grunt) { // folders with '.' in them since the grunt copy task does not like that var componentWithoutPeriod = component.replace(".", "-"); return path.join(componentWithoutPeriod, type); + }, + 'bootstrap-tabdrop': { + keepExpandedHierarchy: false } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js index b1cca01d44..15a1a09c05 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js @@ -40,6 +40,8 @@ angular.module("umbraco.directives") } }); + + $('.nav-pills, .nav-tabs').tabdrop(); } scope.showTabs = iAttrs.tabs ? true : false; diff --git a/src/Umbraco.Web.UI.Client/src/loader.js b/src/Umbraco.Web.UI.Client/src/loader.js index 32def61618..9cbb8f233d 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.js +++ b/src/Umbraco.Web.UI.Client/src/loader.js @@ -1,28 +1,32 @@ LazyLoad.js( [ 'lib/jquery/jquery.min.js', - 'lib/jquery-ui/jquery-ui.min.js', - - /* 1.1.5 */ 'lib/angular/1.1.5/angular.min.js', + 'lib/underscore/underscore-min.js', + + 'lib/jquery-ui/jquery-ui.min.js', + 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-mobile.js', 'lib/angular/1.1.5/angular-sanitize.min.js', 'lib/angular/angular-ui-sortable.js', 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', - /* App-wide file-upload helper */ + 'lib/blueimp-load-image/load-image.all.min.js', 'lib/jquery-file-upload/jquery.fileupload.js', 'lib/jquery-file-upload/jquery.fileupload-process.js', + 'lib/jquery-file-upload/jquery.fileupload-image.js', 'lib/jquery-file-upload/jquery.fileupload-angular.js', - + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/underscore/underscore-min.js', - 'lib/umbraco/Extensions.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', + 'lib/umbraco/Extensions.js', + 'lib/umbraco/NamespaceManager.js', + 'lib/umbraco/LegacyUmbClientMgr.js', + 'lib/umbraco/LegacySpeechBubble.js', 'js/umbraco.servervariables.js', 'js/app.dev.js', diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 3ff9884ba7..51e19b388a 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -20,6 +20,7 @@ '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/umbraco/NamespaceManager.js', From 1a2849dfb0c1ed413f0e8804eab384108b0402ef Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:35:55 +0200 Subject: [PATCH 14/45] fixes merge issues --- src/Umbraco.Web.UI.Client/bower.json | 59 ++++++++++--------- src/Umbraco.Web.UI.Client/gruntFile.js | 5 +- src/Umbraco.Web.UI.Client/src/loader.js | 2 +- src/Umbraco.Web/UI/JavaScript/JsInitialize.js | 2 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 2c4b4d8d86..d6682b0435 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -1,31 +1,34 @@ { - "name": "Umbraco", - "version": "7", - "homepage": "https://github.com/umbraco/Umbraco-CMS", - "authors": [ - "Shannon " - ], - "description": "Umbraco CMS", - "license": "MIT", - "private": true, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "typeahead.js": "~0.10.5", - "underscore": "~1.7.0", - "rgrove-lazyload": "*", - "jquery": "2.0.3", - "jquery-file-upload": "~9.4.0", - "jquery-ui": "1.10.3", - "angular-dynamic-locale": "~0.1.27", - "bootstrap-tabdrop": "~1.0.0" + "name": "Umbraco", + "version": "7", + "homepage": "https://github.com/umbraco/Umbraco-CMS", + "authors": [ + "Shannon " + ], + "description": "Umbraco CMS", + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "typeahead.js": "~0.10.5", + "underscore": "~1.7.0", + "rgrove-lazyload": "*", + "jquery": "2.0.3", + "jquery-file-upload": "~9.4.0", + "jquery-ui": "1.10.3", + "angular-dynamic-locale": "~0.1.27", + "bootstrap-tabdrop": "~1.0.0" }, "exportsOverride": { + "bootstrap-tabdrop": { + "": "build/js/bootstrap-tabdrop.min.js" + }, "rgrove-lazyload": { "": "lazyload.js" }, @@ -39,7 +42,7 @@ "": "tmhDynamicLocale.min.{js,js.map}" }, "jquery": { - "": "jquery.min.{js,map}" + "": "jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" @@ -54,9 +57,9 @@ "blueimp-tmpl": { "ignore": "*.ignore" }, - + "blueimp-canvas-to-blob": { "ignore": "*.ignore" } - } + } } diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index 03b6c6ccbf..a5961f126c 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -418,10 +418,7 @@ module.exports = function (grunt) { //this is the same as 'byComponent', however we will not allow // folders with '.' in them since the grunt copy task does not like that var componentWithoutPeriod = component.replace(".", "-"); - return path.join(componentWithoutPeriod, type); - }, - 'bootstrap-tabdrop': { - keepExpandedHierarchy: false + return path.join(componentWithoutPeriod, type); } } } diff --git a/src/Umbraco.Web.UI.Client/src/loader.js b/src/Umbraco.Web.UI.Client/src/loader.js index 9cbb8f233d..179de16de8 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.js +++ b/src/Umbraco.Web.UI.Client/src/loader.js @@ -21,7 +21,7 @@ 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/bootstrap-tabdrop/bootstrap-tabdrop.min.js', 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 51e19b388a..3ae74ba08b 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -20,7 +20,7 @@ 'lib/jquery-file-upload/jquery.fileupload-angular.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', From 3bc4b50d74c67f39ee5aa9971d0ad656b6b25936 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:36:50 +0200 Subject: [PATCH 15/45] updates version --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 9199867efa..c3a575dc51 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.6 \ No newline at end of file +7.2.7 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 0e3d292c0f..3c73b4d055 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.2.6")] -[assembly: AssemblyInformationalVersion("7.2.6")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.2.7")] +[assembly: AssemblyInformationalVersion("7.2.7")] \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index b84764cd0d..9649fd42e1 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2542,7 +2542,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7260 / - http://localhost:7260 + http://localhost:7270 False False From 46f9c5ff05c727073b77e693f478fb5485562130 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:46:03 +0200 Subject: [PATCH 16/45] Fixes: U4-5052 (U7) Previewing a published page changes icon in content tree to the little green plus indicating changes need to be published even though no changes have been made. --- src/Umbraco.Core/Models/Property.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 8ba30665d0..1dc882674d 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -137,9 +137,23 @@ namespace Umbraco.Core.Models new DelegateEqualityComparer( (o, o1) => { - //Custom comparer for enumerable if it is enumerable if (o == null && o1 == null) return true; + + //custom comparer for strings. + if (o is string || o1 is string) + { + //if one is null and another is empty then they are the same + if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + { + return true; + } + if (o == null || o1 == null) return false; + return o.Equals(o1); + } + if (o == null || o1 == null) return false; + + //Custom comparer for enumerable if it is enumerable var enum1 = o as IEnumerable; var enum2 = o1 as IEnumerable; if (enum1 != null && enum2 != null) From b23ea69b3e86c16e06b021d82ac8d50ffd42c46c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:48:17 +0200 Subject: [PATCH 17/45] Fixes: U4-6636 Left navbar (section-bar) is unnecessary wide on mobile devices Conflicts: src/Umbraco.Web.UI.Client/src/less/grid.less --- src/Umbraco.Web.UI.Client/src/less/grid.less | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/grid.less b/src/Umbraco.Web.UI.Client/src/less/grid.less index 22ea5d9836..7cd8167cd6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/grid.less @@ -176,3 +176,73 @@ body { .emptySection #contentwrapper {left: 80px;} .emptySection #speechbubble {left: 0;} .emptySection #navigation {display: none} + +} + + + +@media (max-width: 767px) { + + // make leftcolumn smaller on tablets + #leftcolumn { + width: 60px; + } + #applications ul.sections { + width: 60px; + } + ul.sections.sections-tray { + width: 60px; + } + #applications-tray { + left: 60px; + } + #navigation { + left: 60px; + } + #contentwrapper, #contentcolumn { + left: 30px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 60px; + } +} + + + +@media (max-width: 500px) { + + // make leftcolumn smaller on mobiles + #leftcolumn { + width: 40px; + } + #applications ul.sections { + width: 40px; + } + ul.sections li [class^="icon-"]:before { + font-size: 25px!important; + } + #applications ul.sections li.avatar a img { + width: 25px; + } + ul.sections a span { + display:none !important; + } + #applications ul.sections-tray { + width: 40px; + } + ul.sections.sections-tray { + width: 40px; + } + #applications-tray { + left: 40px; + } + #navigation { + left: 40px; + } + #contentwrapper, #contentcolumn { + left: 20px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 40px; + width: 85%!important; + } \ No newline at end of file From 044c9c95f8cd5b3d24f61926ba13df4659b29ca6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:49:09 +0200 Subject: [PATCH 18/45] Merge branch 'dev-v7-U4-6677' of https://github.com/bjarnef/Umbraco-CMS into bjarnef-dev-v7-U4-6677 Conflicts: src/Umbraco.Web.UI.Client/src/less/panel.less Conflicts: src/Umbraco.Web.UI.Client/src/less/panel.less --- src/Umbraco.Web.UI.Client/src/less/panel.less | 332 ++++++++++-------- 1 file changed, 194 insertions(+), 138 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 2a6abef6d7..ca61d872d3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -1,19 +1,27 @@ // Panel // ------------------------- -.umb-panel{ - background: white; - position: absolute; - top: 0px; bottom: 0px; left: 0px; right: 0px;} +.umb-panel { + background: white; + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; +} + +.umb-panel-nobody { + padding-top: 100px; + overflow: auto; +} -.umb-panel-nobody{padding-top: 100px; overflow: auto;} .umb-panel-header { - background: @grayLighter; - border-bottom: 1px solid @grayLight; - position: absolute; - height: 99px; - top: 0px; - right: 0px; - left: 0px; + background: @grayLighter; + border-bottom: 1px solid @grayLight; + position: absolute; + height: 99px; + top: 0px; + right: 0px; + left: 0px; } .umb-panel-body { @@ -25,6 +33,7 @@ clear: both; overflow: auto; } + .umb-panel-body.no-header { top: 20px; } @@ -38,71 +47,83 @@ } .umb-panel-header .umb-headline, .umb-panel-header h1 { - font-size: 16px; - border: none; - background: none; - margin: 15px 0 0 20px; - padding: 3px 5px; - line-height: 1.4; - height: auto; - width: 100%; - border: 1px solid @grayLighter; + font-size: 16px; + border: none; + background: none; + margin: 15px 0 0 20px; + padding: 3px 5px; + line-height: 1.4; + height: auto; + width: 100%; + border: 1px solid @grayLighter; } -.umb-panel-header .umb-headline:focus,.umb-panel-header .umb-headline:active { - border: 1px solid @grayLight; - background-color: @white; +.umb-panel-header .umb-headline:focus, .umb-panel-header .umb-headline:active { + border: 1px solid @grayLight; + background-color: @white; } -.umb-headline-editor-wrapper{ - position: relative; +.umb-headline-editor-wrapper { + position: relative; } -.umb-headline-editor-wrapper .help-inline{ - right: 0px; - top: 25px; - position: absolute; - font-size: 10px; - color: @red; - } +.umb-headline-editor-wrapper .help-inline { + right: 0px; + top: 25px; + position: absolute; + font-size: 10px; + color: @red; +} -.umb-panel-header .umb-nav-tabs{ - bottom: -1px; - position: absolute; - padding: 0px 0px 0px 20px; +.umb-panel-header .umb-nav-tabs { + bottom: -1px; + position: absolute; + padding: 0px 0px 0px 20px; } .umb-panel-header p { - margin:0px 20px; + margin: 0px 20px; } -.umb-btn-toolbar .dimmed, .umb-dimmed{ - opacity: 0.6; +.umb-btn-toolbar .dimmed, .umb-dimmed { + opacity: 0.6; } -.umb-headline-editor-wrapper input { - background: none; - border: none; - margin: -6px 0 0 0; - padding: 0 0 2px 0; - border-radius: 0; - line-height: normal; - border: 1px solid transparent; - color: @black; - letter-spacing: -0.01em -} -.umb-headline-editor-wrapper input.ng-invalid { - color: @red; +.umb-headline-editor-wrapper input { + background: none; + border: none; + margin: -6px 0 0 0; + padding: 0 0 2px 0; + border-radius: 0; + line-height: normal; + border: 1px solid transparent; + color: @black; + letter-spacing: -0.01em; } -.umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder {color: @red; line-height: 22px;} -.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder {color: @red; line-height: 22px;} -.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder {color: @red; line-height: 22px;} +.umb-headline-editor-wrapper input.ng-invalid { + color: @red; +} + +.umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder { + color: @red; + line-height: 22px; +} + +.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder { + color: @red; + line-height: 22px; +} + +.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder { + color: @red; + line-height: 22px; +} .umb-panel-header i { - font-size: 13px; - vertical-align: middle; + font-size: 13px; + vertical-align: middle; } .umb-panel-header-meta { @@ -115,65 +136,76 @@ } .umb-panel-footer { - margin: 0; - padding: 20px; - z-index: 999; - position: absolute; - bottom: 0px; - left: 0px; - right: 0px; + margin: 0; + padding: 20px; + z-index: 999; + position: absolute; + bottom: 0px; + left: 0px; + right: 0px; } /* Publish */ .umb-btn-toolbar .dropdown-menu { - right: 0; - left: auto; - border-radius: @tabsBorderRadius; - box-shadow: none; - padding: 0 - } + right: 0; + left: auto; + border-radius: @tabsBorderRadius; + box-shadow: none; + padding: 0; +} - .umb-btn-toolbar .dropdown-menu small { - background: #fef9db; - display: block; - padding: 10px 20px; - } +.umb-btn-toolbar .dropdown-menu small { + background: #fef9db; + display: block; + padding: 10px 20px; +} - .umb-btn-toolbar .dropdown-menu .btn { +.umb-btn-toolbar .dropdown-menu .btn  { margin: 20px 29px; - width: 80px - } + width: 80px; +} /* tab buttons */ -.umb-bottom-bar{ +.umb-bottom-bar { background: white; -webkit-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); - -moz-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); - box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); + -moz-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); + box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); border-top: 1px solid @grayLighter; padding: 10px 0 10px 0; position: fixed; - bottom: 0px; + bottom: 0; left: 100px; + right: 40px; z-index: 6010; -} -.umb-tab-buttons{ - padding-left: 0px; -} - -@media (min-width: 1101px) { - .umb-bottom-bar { + @media (min-width: 1101px) { left: 460px; - } + } } -.umb-tab-pane{padding-bottom: 90px} +.umb-tab-buttons { + padding-left: 0; -.tab-content{overflow: visible; } + > .btn-group:not([style*="display:none"]):not([style*="display: none"]) { + margin-left: 0; + } -.umb-panel-footer-nav{ + @media (min-width: 768px) { + padding-left: 180px; + } +} + +.umb-tab-pane { + padding-bottom: 90px; +} + +.tab-content { + overflow: visible; +} + +.umb-panel-footer-nav { position: absolute; bottom: 0px; height: 30px; @@ -187,93 +219,117 @@ } .umb-panel-footer-nav li a { - border-radius: 0; - display: block; - float: left; - height: 30px; - background: @grayLighter; - text-align: center; - padding: 8px 0px 8px 30px; - position: relative; - margin: 0 1px 0 0; - text-decoration: none; - color: @gray; - font-size: 11px; + border-radius: 0; + display: block; + float: left; + height: 30px; + background: @grayLighter; + text-align: center; + padding: 8px 0px 8px 30px; + position: relative; + margin: 0 1px 0 0; + text-decoration: none; + color: @gray; + font-size: 11px; } .umb-panel-footer-nav li a:after { - content: ""; - border-top: 16px solid transparent; - border-bottom: 16px solid transparent; - border-left: 16px solid @grayLighter; - position: absolute; right: -16px; top: 0; - z-index: 1; -} - -.umb-panel-footer-nav li a:before { - content: ""; - border-top: 16px solid transparent; - border-bottom: 16px solid transparent; - border-left: 16px solid @grayLight; - position: absolute; left: 0; top: 0; + content: ""; + border-top: 16px solid transparent; + border-bottom: 16px solid transparent; + border-left: 16px solid @grayLighter; + position: absolute; + right: -16px; + top: 0; + z-index: 1; } -.umb-panel-footer-nav li:first-child a{ - padding-left: 20px; +.umb-panel-footer-nav li a:before { + content: ""; + border-top: 16px solid transparent; + border-bottom: 16px solid transparent; + border-left: 16px solid @grayLight; + position: absolute; + left: 0; + top: 0; +} + +.umb-panel-footer-nav li:first-child a { + padding-left: 20px; } .umb-panel-footer-nav li:first-child a:before { - display: none; + display: none; } .umb-panel-footer-nav li:last-child a:after { - display: none; + display: none; } - // Utility classes +// Utility classes - //SD: Had to add these because if we want to use the bootstrap text colors - // in a panel/editor they'll all show up as white on white - so we need to use the - // form styles +//SD: Had to add these because if we want to use the bootstrap text colors +// in a panel/editor they'll all show up as white on white - so we need to use the +// form styles .umb-dialog .muted, -.umb-panel .muted { color: @grayLight; } +.umb-panel .muted { + color: @grayLight; +} .umb-dialog a.muted:hover, .umb-dialog a.muted:focus, .umb-panel a.muted:hover, -.umb-panel a.muted:focus { color: darken(@grayLight, 10%); } +.umb-panel a.muted:focus { + color: darken(@grayLight, 10%); +} .umb-dialog .text-warning, -.umb-panel .text-warning { color: @formWarningText; } +.umb-panel .text-warning { + color: @formWarningText; +} .umb-dialog a.text-warning:hover, .umb-dialog a.text-warning:focus, .umb-panel a.text-warning:hover, -.umb-panel a.text-warning:focus { color: darken(@formWarningText, 10%); } +.umb-panel a.text-warning:focus { + color: darken(@formWarningText, 10%); +} .umb-dialog .text-error, -.umb-panel .text-error { color: @formErrorText; } +.umb-panel .text-error { + color: @formErrorText; +} .umb-dialog a.text-error:hover, .umb-dialog a.text-error:focus, .umb-panel a.text-error:hover, -.umb-panel a.text-error:focus { color: darken(@formErrorText, 10%); } +.umb-panel a.text-error:focus { + color: darken(@formErrorText, 10%); +} .umb-dialog .text-info, -.umb-panel .text-info { color: @formInfoText; } +.umb-panel .text-info { + color: @formInfoText; +} .umb-dialog a.text-info:hover, .umb-dialog a.text-info:focus, .umb-panel a.text-info:hover, -.umb-panel a.text-info:focus { color: darken(@formInfoText, 10%); } +.umb-panel a.text-info:focus { + color: darken(@formInfoText, 10%); +} .umb-dialog .text-success, -.umb-panel .text-success { color: @formSuccessText; } +.umb-panel .text-success { + color: @formSuccessText; +} .umb-dialog a.text-success:hover, .umb-dialog a.text-success:focus, .umb-panel a.text-success:hover, -.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } \ No newline at end of file +.umb-panel a.text-success:focus { + color: darken(@formSuccessText, 10%); +} \ No newline at end of file From 053346d79ba260617538ba7e3fbe125057603d73 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 11:54:10 +0200 Subject: [PATCH 19/45] fixes merge --- src/Umbraco.Web.UI.Client/src/less/grid.less | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/grid.less b/src/Umbraco.Web.UI.Client/src/less/grid.less index 7cd8167cd6..8e1557fea1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/grid.less @@ -177,10 +177,6 @@ body { .emptySection #speechbubble {left: 0;} .emptySection #navigation {display: none} -} - - - @media (max-width: 767px) { // make leftcolumn smaller on tablets @@ -245,4 +241,5 @@ body { #umbracoMainPageBody .umb-modal-left.fade.in { margin-left: 40px; width: 85%!important; - } \ No newline at end of file + } +} \ No newline at end of file From 8cfb56adf47b9db12002743be2213711c309cde1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 13:28:15 +0200 Subject: [PATCH 20/45] adds machine name logging to startup --- src/Umbraco.Core/CoreBootManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 6e00d3d682..740df8015d 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -66,7 +66,7 @@ namespace Umbraco.Core InitializeProfilerResolver(); - _timer = DisposableTimer.TraceDuration("Umbraco application starting", "Umbraco application startup complete"); + _timer = DisposableTimer.TraceDuration("Umbraco application starting on '" + NetworkHelper.MachineName + "'", "Umbraco application startup complete"); CreateApplicationCache(); From ca738979ce6987d5dc6a9607944ee2d6955dadb4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 16:27:06 +0200 Subject: [PATCH 21/45] Adds some more detailed logging. --- src/Umbraco.Core/CoreBootManager.cs | 39 ++++++++++++++++++++-- src/Umbraco.Core/PluginManager.cs | 12 +++---- src/Umbraco.Core/UmbracoApplicationBase.cs | 37 ++++++++++++++++++-- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 740df8015d..5020086700 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -97,7 +97,18 @@ namespace Umbraco.Core //now we need to call the initialize methods ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationInitialized(UmbracoApplication, ApplicationContext)); + .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; @@ -203,7 +214,18 @@ namespace Umbraco.Core //call OnApplicationStarting of each application events handler ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarting(UmbracoApplication, ApplicationContext)); + .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) { @@ -229,7 +251,18 @@ namespace Umbraco.Core //call OnApplicationStarting of each application events handler ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarted(UmbracoApplication, ApplicationContext)); + .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(); diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index d03edc0fc6..c63d8cdfde 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -606,14 +606,10 @@ namespace Umbraco.Core /// internal IEnumerable CreateInstances(IEnumerable types, bool throwException = false) { - //Have removed logging because it doesn't really need to be logged since the time taken is generally 0ms. - //we want to know if it fails ever, not how long it took if it is only 0. - var typesAsArray = types.ToArray(); - //using (DisposableTimer.DebugDuration( - // String.Format("Starting instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName), - // String.Format("Completed instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName))) - //{ + + LogHelper.Debug(string.Format("Instantiating {0} objects of type {1}", typesAsArray.Length, typeof (T).FullName)); + var instances = new List(); foreach (var t in typesAsArray) { @@ -634,7 +630,7 @@ namespace Umbraco.Core } } return instances; - //} + } /// diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index c861eca6cd..07e3a6bbf3 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -73,7 +73,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; + } + } + } /// @@ -84,7 +95,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; + } + } } /// @@ -95,7 +116,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; + } + } } /// From 9b85ba5b908538e3b1ffd035bc82ac862f9a4932 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 8 Jul 2015 16:28:35 +0200 Subject: [PATCH 22/45] Resolution - better logging when freezing --- .../ObjectResolution/Resolution.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/ObjectResolution/Resolution.cs b/src/Umbraco.Core/ObjectResolution/Resolution.cs index 87eb06e295..0b478d54cf 100644 --- a/src/Umbraco.Core/ObjectResolution/Resolution.cs +++ b/src/Umbraco.Core/ObjectResolution/Resolution.cs @@ -112,7 +112,7 @@ namespace Umbraco.Core.ObjectResolution /// resolution is already frozen. public static void Freeze() { - LogHelper.Debug(typeof(Resolution), "Freezing resolution"); + LogHelper.Debug(typeof (Resolution), "Freezing resolution"); using (new WriteLock(ConfigurationLock)) { @@ -121,9 +121,20 @@ namespace Umbraco.Core.ObjectResolution _isFrozen = true; } - - if (Frozen != null) - Frozen(null, null); + + LogHelper.Debug(typeof(Resolution), "Resolution is frozen"); + + if (Frozen == null) return; + + try + { + Frozen(null, null); + } + catch (Exception e) + { + LogHelper.Error(typeof (Resolution), "Exception in Frozen event handler.", e); + throw; + } } /// From 0520797c0928bee8dc71e0630be3e983359d1b62 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 16:45:34 +0200 Subject: [PATCH 23/45] more logging --- src/Umbraco.Core/CoreBootManager.cs | 99 +++++++++++++++++------------ 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 5020086700..c6591b7e47 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -95,20 +95,25 @@ namespace Umbraco.Core InitializeModelMappers(); - //now we need to call the initialize methods - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => - { - try + 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 => { - x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred running OnApplicationInitialized for handler " + x.GetType(), ex); - throw; - } - }); + try + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationInitialized for handler " + x.GetType(), ex); + throw; + } + }); + } _isInitialized = true; @@ -212,22 +217,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 => - { - try - { - x.OnApplicationStarting(UmbracoApplication, ApplicationContext); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred running OnApplicationStarting for handler " + x.GetType(), ex); - throw; - } - }); + 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); } @@ -248,21 +258,26 @@ namespace Umbraco.Core throw new InvalidOperationException("The boot manager has already been completed"); FreezeResolution(); - - //call OnApplicationStarting of each application events handler - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => - { - try + + 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 => { - x.OnApplicationStarted(UmbracoApplication, ApplicationContext); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred running OnApplicationStarted for handler " + x.GetType(), ex); - throw; - } - }); + 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(); From 7cd39918a0106177ec7b3c2010933b5c7931967f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 16:54:38 +0200 Subject: [PATCH 24/45] another log entry --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 8655acd9a7..a3be346758 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -14,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.member; 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; @@ -30,7 +31,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; From 69428b228101716d34dc3be016c00db0102af718 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:22:44 +0200 Subject: [PATCH 25/45] Scheduled publishing is executing request async with cancellation token --- .../Mvc/AdminTokenAuthorizeAttribute.cs | 18 +++++-- .../Scheduling/BackgroundTaskRunner.cs | 15 ++++++ .../Scheduling/ScheduledPublishing.cs | 47 ++++++++++++++----- src/Umbraco.Web/Scheduling/Scheduler.cs | 2 +- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs index cbd2e0e519..dc8aa15f6a 100644 --- a/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Web; @@ -36,22 +37,33 @@ namespace Umbraco.Web.Mvc return _applicationContext ?? ApplicationContext.Current; } + public const string AuthorizationType = "AToken"; + /// - /// Used to return the value that needs to go in the Authorization header + /// Used to return the full value that needs to go in the Authorization header /// /// /// public static string GetAuthHeaderTokenVal(ApplicationContext appContext) { - var admin = appContext.Services.UserService.GetUserById(0); + return string.Format("{0} {1}", AuthorizationType, GetAuthHeaderVal(appContext)); + } + public static AuthenticationHeaderValue GetAuthenticationHeaderValue(ApplicationContext appContext) + { + return new AuthenticationHeaderValue(AuthorizationType, GetAuthHeaderVal(appContext)); + } + + private static string GetAuthHeaderVal(ApplicationContext appContext) + { + var admin = appContext.Services.UserService.GetUserById(0); var token = string.Format("{0}u____u{1}u____u{2}", admin.Email, admin.Username, admin.RawPasswordValue); var encrypted = token.EncryptWithMachineKey(); var bytes = Encoding.UTF8.GetBytes(encrypted); var base64 = Convert.ToBase64String(bytes); - return "AToken val=\"" + base64 + "\""; + return string.Format("val=\"{0}\"", base64); } /// diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 575845efba..78e5f4ce74 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -96,6 +96,21 @@ namespace Umbraco.Web.Scheduling StartUp(); } + /// + /// Returns the current cancellation token + /// + public CancellationToken CurrentCancellationToken + { + get + { + if (_tokenSource == null) + { + throw new InvalidOperationException("The token source has not been created which means the task runner has not been started"); + } + return _tokenSource.Token; + } + } + /// /// Gets the number of tasks in the queue. /// diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 9db21fba8a..cbc000b7e6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -1,7 +1,10 @@ using System; using System.Diagnostics; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; +using System.Threading; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; @@ -16,15 +19,17 @@ namespace Umbraco.Web.Scheduling { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; + private readonly Func _cancellationToken; private static bool _isPublishingRunning; public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - ApplicationContext appContext, IUmbracoSettingsSection settings) + ApplicationContext appContext, IUmbracoSettingsSection settings, Func cancellationToken) : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; + _cancellationToken = cancellationToken; } private ScheduledPublishing(ScheduledPublishing source) @@ -41,6 +46,12 @@ namespace Umbraco.Web.Scheduling public override void PerformRun() { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync() + { + if (_appContext == null) return; if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { @@ -57,7 +68,7 @@ namespace Umbraco.Web.Scheduling var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(_appContext, _settings); try - { + { if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) { @@ -66,13 +77,30 @@ namespace Umbraco.Web.Scheduling else { var url = string.Format("{0}RestServices/ScheduledPublish/Index", umbracoBaseUrl.EnsureEndsWith('/')); - using (var wc = new WebClient()) + using (var wc = new HttpClient()) { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(url), + Method = HttpMethod.Post, + Content = new StringContent(string.Empty) + }; //pass custom the authorization header - wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(_appContext)); + request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - var result = wc.UploadString(url, ""); - } + var token = new CancellationToken(); + try + { + token = _cancellationToken(); + } + catch (InvalidOperationException) + { + //There is no valid token, so we'll continue with the empty one + } + + var result = await wc.SendAsync(request, token); + + } } } catch (Exception ee) @@ -88,14 +116,9 @@ namespace Umbraco.Web.Scheduling } } - public override Task PerformRunAsync() - { - throw new NotImplementedException(); - } - public override bool IsAsync { - get { return false; } + get { return true; } } public override bool RunsOnShutdown diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 409a67028f..71f2ab59c5 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -66,7 +66,7 @@ namespace Umbraco.Web.Scheduling // scheduled publishing/unpublishing // install on all, will only run on non-slaves servers // both are delayed recurring tasks - _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); + _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings, () => _publishingRunner.CurrentCancellationToken)); _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); // log scrubbing From 76a6f2d2d08de65a4869a2f2302310fa7b54c520 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:23:00 +0200 Subject: [PATCH 26/45] Updates scheduled tasks to be async with cancellation token --- .../Scheduling/ScheduledPublishing.cs | 10 ++- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 72 ++++++++++++------- src/Umbraco.Web/Scheduling/Scheduler.cs | 2 +- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index cbc000b7e6..c60226965f 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -98,8 +98,14 @@ namespace Umbraco.Web.Scheduling //There is no valid token, so we'll continue with the empty one } - var result = await wc.SendAsync(request, token); - + try + { + var result = await wc.SendAsync(request, token); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling scheduled publish url", ex); + } } } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index cba3cb4fc8..600cc499de 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -2,6 +2,8 @@ using System; using System.Collections; using System.Linq; using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using System.Xml; using Umbraco.Core.Configuration; @@ -21,15 +23,17 @@ namespace Umbraco.Web.Scheduling { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; + private readonly Func _cancellationToken; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); private static bool _isPublishingRunning = false; public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - ApplicationContext appContext, IUmbracoSettingsSection settings) + ApplicationContext appContext, IUmbracoSettingsSection settings, Func cancellationToken) : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; + _cancellationToken = cancellationToken; } public ScheduledTasks(ScheduledTasks source) @@ -44,18 +48,19 @@ namespace Umbraco.Web.Scheduling return new ScheduledTasks(this); } - private void ProcessTasks() + private async Task ProcessTasksAsync() { var scheduledTasks = _settings.ScheduledTasks.Tasks; foreach (var t in scheduledTasks) { var runTask = false; - if (!ScheduledTaskTimes.ContainsKey(t.Alias)) + if (ScheduledTaskTimes.ContainsKey(t.Alias) == false) { runTask = true; ScheduledTaskTimes.Add(t.Alias, DateTime.Now); } - /// Add 1 second to timespan to compensate for differencies in timer + + // Add 1 second to timespan to compensate for differencies in timer else if ( new TimeSpan( DateTime.Now.Ticks - ((DateTime)ScheduledTaskTimes[t.Alias]).Ticks).TotalSeconds + 1 >= t.Interval) @@ -66,33 +71,55 @@ namespace Umbraco.Web.Scheduling if (runTask) { - bool taskResult = GetTaskByHttp(t.Url); + bool taskResult = await GetTaskByHttpAync(t.Url); if (t.Log) LogHelper.Info(string.Format("{0} has been called with response: {1}", t.Alias, taskResult)); } } } - private bool GetTaskByHttp(string url) + private async Task GetTaskByHttpAync(string url) { - var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); - - try + using (var wc = new HttpClient()) { - using (var response = (HttpWebResponse)myHttpWebRequest.GetResponse()) + var request = new HttpRequestMessage() { - return response.StatusCode == HttpStatusCode.OK; - } - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling web task for url: " + url, ex); - } + RequestUri = new Uri(url), + Method = HttpMethod.Get, + Content = new StringContent(string.Empty) + }; + //TODO: pass custom the authorization header, currently these aren't really secured! + //request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - return false; + var token = new CancellationToken(); + try + { + token = _cancellationToken(); + } + catch (InvalidOperationException) + { + //There is no valid token, so we'll continue with the empty one + } + + try + { + var result = await wc.SendAsync(request, token); + return result.StatusCode == HttpStatusCode.OK; + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); + } + return false; + } } public override void PerformRun() + { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync() { if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { @@ -108,7 +135,7 @@ namespace Umbraco.Web.Scheduling try { - ProcessTasks(); + await ProcessTasksAsync(); } catch (Exception ee) { @@ -121,14 +148,9 @@ namespace Umbraco.Web.Scheduling } } - public override Task PerformRunAsync() - { - throw new NotImplementedException(); - } - public override bool IsAsync { - get { return false; } + get { return true; } } public override bool RunsOnShutdown diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 71f2ab59c5..904254ee79 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.Scheduling // install on all, will only run on non-slaves servers // both are delayed recurring tasks _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings, () => _publishingRunner.CurrentCancellationToken)); - _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); + _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings, () => _tasksRunner.CurrentCancellationToken)); // log scrubbing // install & run on all servers From ccda1bdd73503dbb8e6b4d2a4c716b39ac47eae7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:23:15 +0200 Subject: [PATCH 27/45] Refactor scheduled tasks & publishing cancellation --- .../Scheduling/BackgroundTaskRunnerTests.cs | 4 +-- .../Scheduling/BackgroundTaskRunner.cs | 15 --------- src/Umbraco.Web/Scheduling/LogScrubber.cs | 3 +- .../Scheduling/RecurringTaskBase.cs | 5 +-- .../Scheduling/ScheduledPublishing.cs | 21 ++----------- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 31 +++++-------------- src/Umbraco.Web/Scheduling/Scheduler.cs | 4 +-- 7 files changed, 19 insertions(+), 64 deletions(-) diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 52962879d4..80ecc588f4 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -805,7 +805,7 @@ namespace Umbraco.Tests.Scheduling HasRun = true; } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } @@ -889,7 +889,7 @@ namespace Umbraco.Tests.Scheduling Thread.Sleep(_runMilliseconds); } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 78e5f4ce74..575845efba 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -96,21 +96,6 @@ namespace Umbraco.Web.Scheduling StartUp(); } - /// - /// Returns the current cancellation token - /// - public CancellationToken CurrentCancellationToken - { - get - { - if (_tokenSource == null) - { - throw new InvalidOperationException("The token source has not been created which means the task runner has not been started"); - } - return _tokenSource.Token; - } - } - /// /// Gets the number of tasks in the queue. /// diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index a9f70a612e..79b4c0f47b 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Caching; @@ -73,7 +74,7 @@ namespace Umbraco.Web.Scheduling } } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index d710a70e03..dc82795852 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -55,7 +55,7 @@ namespace Umbraco.Web.Scheduling /// Classes inheriting from RecurringTaskBase must implement PerformRun. public virtual async Task RunAsync(CancellationToken token) { - await PerformRunAsync(); + await PerformRunAsync(token); Repeat(); } @@ -95,8 +95,9 @@ namespace Umbraco.Web.Scheduling /// /// Runs the task asynchronously. /// + /// A cancellation token. /// A instance representing the execution of the background task. - public abstract Task PerformRunAsync(); + public abstract Task PerformRunAsync(CancellationToken token); /// /// Gets a new occurence of the recurring task. diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index c60226965f..0f7cb8c205 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -1,15 +1,10 @@ using System; -using System.Diagnostics; -using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; using System.Threading; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Publishing; using Umbraco.Core.Sync; using Umbraco.Web.Mvc; @@ -19,17 +14,15 @@ namespace Umbraco.Web.Scheduling { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private readonly Func _cancellationToken; private static bool _isPublishingRunning; public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - ApplicationContext appContext, IUmbracoSettingsSection settings, Func cancellationToken) + ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; - _cancellationToken = cancellationToken; } private ScheduledPublishing(ScheduledPublishing source) @@ -49,7 +42,7 @@ namespace Umbraco.Web.Scheduling throw new NotImplementedException(); } - public override async Task PerformRunAsync() + public override async Task PerformRunAsync(CancellationToken token) { if (_appContext == null) return; @@ -88,16 +81,6 @@ namespace Umbraco.Web.Scheduling //pass custom the authorization header request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - var token = new CancellationToken(); - try - { - token = _cancellationToken(); - } - catch (InvalidOperationException) - { - //There is no valid token, so we'll continue with the empty one - } - try { var result = await wc.SendAsync(request, token); diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 600cc499de..1015b2d4f6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -1,17 +1,13 @@ using System; using System.Collections; -using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using System.Xml; -using Umbraco.Core.Configuration; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Publishing; using Umbraco.Core.Sync; -using Umbraco.Core; namespace Umbraco.Web.Scheduling { @@ -23,17 +19,15 @@ namespace Umbraco.Web.Scheduling { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private readonly Func _cancellationToken; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); private static bool _isPublishingRunning = false; public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - ApplicationContext appContext, IUmbracoSettingsSection settings, Func cancellationToken) + ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; - _cancellationToken = cancellationToken; } public ScheduledTasks(ScheduledTasks source) @@ -48,7 +42,7 @@ namespace Umbraco.Web.Scheduling return new ScheduledTasks(this); } - private async Task ProcessTasksAsync() + private async Task ProcessTasksAsync(CancellationToken token) { var scheduledTasks = _settings.ScheduledTasks.Tasks; foreach (var t in scheduledTasks) @@ -71,14 +65,14 @@ namespace Umbraco.Web.Scheduling if (runTask) { - bool taskResult = await GetTaskByHttpAync(t.Url); + var taskResult = await GetTaskByHttpAync(t.Url, token); if (t.Log) LogHelper.Info(string.Format("{0} has been called with response: {1}", t.Alias, taskResult)); } } } - private async Task GetTaskByHttpAync(string url) + private async Task GetTaskByHttpAync(string url, CancellationToken token) { using (var wc = new HttpClient()) { @@ -88,19 +82,10 @@ namespace Umbraco.Web.Scheduling Method = HttpMethod.Get, Content = new StringContent(string.Empty) }; + //TODO: pass custom the authorization header, currently these aren't really secured! //request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - var token = new CancellationToken(); - try - { - token = _cancellationToken(); - } - catch (InvalidOperationException) - { - //There is no valid token, so we'll continue with the empty one - } - try { var result = await wc.SendAsync(request, token); @@ -119,7 +104,7 @@ namespace Umbraco.Web.Scheduling throw new NotImplementedException(); } - public override async Task PerformRunAsync() + public override async Task PerformRunAsync(CancellationToken token) { if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { @@ -135,7 +120,7 @@ namespace Umbraco.Web.Scheduling try { - await ProcessTasksAsync(); + await ProcessTasksAsync(token); } catch (Exception ee) { diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 904254ee79..409a67028f 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -66,8 +66,8 @@ namespace Umbraco.Web.Scheduling // scheduled publishing/unpublishing // install on all, will only run on non-slaves servers // both are delayed recurring tasks - _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings, () => _publishingRunner.CurrentCancellationToken)); - _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings, () => _tasksRunner.CurrentCancellationToken)); + _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); + _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); // log scrubbing // install & run on all servers From 7987f13a1150818dffe78c2269f955f73f850468 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:32:36 +0200 Subject: [PATCH 28/45] U4-6788 - refactor OriginalRequestUrl into UmbracoApplicationUrl and fixes merge issues. --- src/Umbraco.Core/ApplicationContext.cs | 45 +++++++-- .../UmbracoSettings/IWebRoutingSection.cs | 2 + .../UmbracoSettings/WebRoutingElement.cs | 7 +- .../Sync/ServerEnvironmentHelper.cs | 92 +++++++++---------- .../ServerEnvironmentHelperTests.cs | 78 ++++++++-------- src/Umbraco.Web/Scheduling/KeepAlive.cs | 36 +++----- .../Scheduling/ScheduledPublishing.cs | 49 +++++----- src/Umbraco.Web/UmbracoModule.cs | 58 +++++++----- .../umbraco.presentation/keepAliveService.cs | 4 +- 9 files changed, 206 insertions(+), 165 deletions(-) diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 382c3f29c4..c08cb0e353 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Services; +using Umbraco.Core.Sync; namespace Umbraco.Core @@ -154,17 +155,47 @@ namespace Umbraco.Core } /// - /// The original/first url that the web application executes + /// The application url. /// /// - /// we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must - /// exist on a global context because the keep alive service doesn't run in a web context. - /// we are NOT going to put a lock on this because locking will slow down the application and we don't really care - /// if two threads write to this at the exact same time during first page hit. - /// see: http://issues.umbraco.org/issue/U4-2059 + /// The application url is the url that should be used by services to talk to the application, + /// eg keep alive or scheduled publishing services. It must exist on a global context because + /// some of these services may not run within a web context. + /// The format of the application url is: + /// - has a scheme (http or https) + /// - has the SystemDirectories.Umbraco path + /// - does not end with a slash + /// It is initialized on the first request made to the server, by UmbracoModule.EnsureApplicationUrl: + /// - if umbracoSettings:settings/web.routing/@appUrl is set, use the value (new setting) + /// - if umbracoSettings:settings/scheduledTasks/@baseUrl is set, use the value (backward compatibility) + /// - otherwise, use the url of the (first) request. + /// Not locking, does not matter if several threads write to this. + /// See also issues: + /// - http://issues.umbraco.org/issue/U4-2059 + /// - http://issues.umbraco.org/issue/U4-6788 + /// - http://issues.umbraco.org/issue/U4-5728 + /// - http://issues.umbraco.org/issue/U4-5391 /// - internal string OriginalRequestUrl { get; set; } + internal string UmbracoApplicationUrl + { + get + { + // if initialized, return + if (_umbracoApplicationUrl != null) return _umbracoApplicationUrl; + // try settings + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(this, UmbracoConfig.For.UmbracoSettings()); + + // and return what we have, may be null + return _umbracoApplicationUrl; + } + set + { + _umbracoApplicationUrl = value; + } + } + + internal string _umbracoApplicationUrl; // internal for tests private bool _versionsDifferenceReported; /// diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs index f3d42b6904..2998fc2f78 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs @@ -11,6 +11,8 @@ bool DisableFindContentByIdPath { get; } string UrlProviderMode { get; } + + string UmbracoApplicationUrl { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs index f5b71eb2c7..1ed9bc034c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs @@ -30,8 +30,13 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("urlProviderMode", DefaultValue = "AutoLegacy")] public string UrlProviderMode { - get { return (string)base["urlProviderMode"]; } + get { return (string) base["urlProviderMode"]; } } + [ConfigurationProperty("umbracoApplicationUrl", DefaultValue = null)] + public string UmbracoApplicationUrl + { + get { return (string)base["umbracoApplicationUrl"]; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs index 1d5ee7855a..120326e3e2 100644 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -1,10 +1,9 @@ -using System; using System.Linq; using System.Web; -using System.Xml; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core.Sync { @@ -13,58 +12,73 @@ namespace Umbraco.Core.Sync /// internal static class ServerEnvironmentHelper { - /// - /// Returns the current umbraco base url for the current server depending on it's environment - /// status. This will attempt to determine the internal umbraco base url that can be used by the current - /// server to send a request to itself if it is in a load balanced environment. - /// - /// The full base url including schema (i.e. http://myserver:80/umbraco ) - or null if the url - /// cannot be determined at the moment (usually because the first request has not properly completed yet). - public static string GetCurrentServerUmbracoBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) + public static void TrySetApplicationUrlFromSettings(ApplicationContext appContext, IUmbracoSettingsSection settings) { + // try umbracoSettings:settings/web.routing/@umbracoApplicationUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - contain a scheme + // - end or not with a slash, it will be taken care of + // eg "http://www.mysite.com/umbraco" + var url = settings.WebRouting.UmbracoApplicationUrl; + if (url.IsNullOrWhiteSpace() == false) + { + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); + return; + } + + // try umbracoSettings:settings/scheduledTasks/@baseUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - NOT contain any scheme (because, legacy) + // - end or not with a slash, it will be taken care of + // eg "mysite.com/umbraco" + url = settings.ScheduledTasks.BaseUrl; + if (url.IsNullOrWhiteSpace() == false) + { + var ssl = GlobalSettings.UseSSL ? "s" : ""; + url = "http" + ssl + "://" + url; + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); + return; + } + + // try servers var status = GetStatus(settings); - if (status == CurrentServerEnvironmentStatus.Single) - { - // single install, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); - } + return; + // no server, nothing we can do var servers = settings.DistributedCall.Servers.ToArray(); + if (servers.Length == 0) + return; - if (servers.Any() == false) - { - // cannot be determined, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); - } - + // we have servers, look for this server foreach (var server in servers) { var appId = server.AppId; var serverName = server.ServerName; + // skip if no data if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) - { continue; - } + // if this server, build and return the url if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) { - //match by appId or computer name! return the url configured - return string.Format("{0}://{1}:{2}/{3}", + // match by appId or computer name, return the url configured + url = string.Format("{0}://{1}:{2}/{3}", server.ForceProtocol.IsNullOrWhiteSpace() ? "http" : server.ForceProtocol, server.ServerAddress, server.ForcePortnumber.IsNullOrWhiteSpace() ? "80" : server.ForcePortnumber, IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); + + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using distributedCall/servers)"); } } - - // cannot be determined, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); } /// @@ -113,21 +127,5 @@ namespace Umbraco.Core.Sync return CurrentServerEnvironmentStatus.Slave; } - - private static string GetBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) - { - return ( - // is config empty? - settings.ScheduledTasks.BaseUrl.IsNullOrWhiteSpace() - // is the orig req empty? - ? appContext.OriginalRequestUrl.IsNullOrWhiteSpace() - // we've got nothing - ? null - //the orig req url is not null, use that - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl) - // the config has been specified, use that - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", settings.ScheduledTasks.BaseUrl)) - .EnsureEndsWith('/'); - } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs index 237c6cb17d..39f8b71eb3 100644 --- a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs +++ b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs @@ -1,92 +1,98 @@ using System.Configuration; +using System.IO; using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Profiling; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests { [TestFixture] public class ServerEnvironmentHelperTests { + + // note: in tests, read appContext._umbracoApplicationUrl and not the property, + // because reading the property does run some code, as long as the field is null. + [Test] - public void Get_Base_Url_Single_Server_Orig_Request_Url_No_SSL() + public void SetApplicationUrlWhenNoSettings() { var appContext = new ApplicationContext(null) { - OriginalRequestUrl = "test.com" + UmbracoApplicationUrl = null // NOT set }; - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.ScheduledTasks == Mock.Of())); - - - Assert.AreEqual("http://test.com/", result); - } - - [Test] - public void Get_Base_Url_Single_Server_Orig_Request_Url_With_SSL() - { - var appContext = new ApplicationContext(null) - { - OriginalRequestUrl = "test.com" - }; - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); - - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) && section.ScheduledTasks == Mock.Of())); - Assert.AreEqual("https://test.com/", result); + // still NOT set + Assert.IsNull(appContext._umbracoApplicationUrl); } [Test] - public void Get_Base_Url_Single_Server_Via_Config_Url_No_SSL() + public void SetApplicationUrlFromDcSettingsNoSsl() { var appContext = new ApplicationContext(null); ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world/"))); - Assert.AreEqual("http://mycoolhost.com/hello/world/", result); + Assert.AreEqual("http://mycoolhost.com/hello/world", appContext._umbracoApplicationUrl); } [Test] - public void Get_Base_Url_Single_Server_Via_Config_Url_With_SSL() + public void SetApplicationUrlFromDcSettingsSsl() { var appContext = new ApplicationContext(null); ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); - Assert.AreEqual("https://mycoolhost.com/hello/world/", result); + Assert.AreEqual("https://mycoolhost.com/hello/world", appContext._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlFromWrSettingsSsl() + { + var appContext = new ApplicationContext(null); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here + + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == "httpx://whatever.com/hello/world/") + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + + + Assert.AreEqual("httpx://whatever.com/hello/world", appContext._umbracoApplicationUrl); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index c1b43b57c6..a47f8aa72c 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -1,10 +1,8 @@ using System; using System.Net; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -13,32 +11,28 @@ namespace Umbraco.Web.Scheduling public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings) { using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) - { - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, - settings); - - if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) + { + var umbracoAppUrl = appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) { LogHelper.Warn("No url for service (yet), skip."); + return; } - else - { - var url = string.Format("{0}ping.aspx", umbracoBaseUrl.EnsureEndsWith('/')); - try + var url = umbracoAppUrl + "/ping.aspx"; + + try + { + using (var wc = new WebClient()) { - using (var wc = new WebClient()) - { - wc.DownloadString(url); - } + wc.DownloadString(url); } - catch (Exception ee) - { - LogHelper.Error( - string.Format("Error in ping. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoBaseUrl) + } + catch (Exception ee) + { + LogHelper.Error( + string.Format("Error in ping. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) , ee); - } } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 0f7cb8c205..d1dc8d1935 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -58,44 +58,41 @@ namespace Umbraco.Web.Scheduling _isPublishingRunning = true; - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(_appContext, _settings); + var umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + LogHelper.Warn("No url for service (yet), skip."); + return; + } try { - - if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) + var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; + using (var wc = new HttpClient()) { - LogHelper.Warn("No url for service (yet), skip."); - } - else - { - var url = string.Format("{0}RestServices/ScheduledPublish/Index", umbracoBaseUrl.EnsureEndsWith('/')); - using (var wc = new HttpClient()) + var request = new HttpRequestMessage() { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(url), - Method = HttpMethod.Post, - Content = new StringContent(string.Empty) - }; - //pass custom the authorization header - request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + RequestUri = new Uri(url), + Method = HttpMethod.Post, + Content = new StringContent(string.Empty) + }; + //pass custom the authorization header + request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - try - { - var result = await wc.SendAsync(request, token); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling scheduled publish url", ex); - } + try + { + var result = await wc.SendAsync(request, token); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling scheduled publish url", ex); } } } catch (Exception ee) { LogHelper.Error( - string.Format("An error occurred with the scheduled publishing. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoBaseUrl) + string.Format("An error occurred with the scheduled publishing. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) , ee); } finally diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 6efe4d4d6a..fd75430ec0 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -36,37 +36,47 @@ namespace Umbraco.Web { #region HttpModule event handlers + private static void EnsureApplicationUrl(HttpRequestBase request) + { + var appctx = ApplicationContext.Current; + + // already initialized = ok + // note that getting ApplicationUrl will ALSO try the various settings + if (appctx.UmbracoApplicationUrl.IsNullOrWhiteSpace() == false) return; + + // so if we reach that point, nothing was configured + // use the current request as application url + + // if (HTTP and SSL not required) or (HTTPS and SSL required), + // use ports from request + // otherwise, + // if non-standard ports used, + // user may need to set baseUrl manually per + // http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks + // TODO update the doc, prefer web.routing/@appUrl to scheduledTasks/@baseUrl + var port = (request.IsSecureConnection == false && GlobalSettings.UseSSL == false) + || (request.IsSecureConnection && GlobalSettings.UseSSL) + ? ":" + request.ServerVariables["SERVER_PORT"] + : ""; + + var ssl = GlobalSettings.UseSSL ? "s" : ""; // force, whatever the first request + var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); + + appctx.UmbracoApplicationUrl = UriUtility.TrimPathEndSlash(url); + LogHelper.Info("ApplicationUrl: " + appctx.UmbracoApplicationUrl + " (UmbracoModule request)"); + } + + /// /// Begins to process a request. /// /// static void BeginRequest(HttpContextBase httpContext) { + // ensure application url is initialized + EnsureApplicationUrl(httpContext.Request); - //we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must - //exist on a global context because the keep alive service doesn't run in a web context. - //we are NOT going to put a lock on this because locking will slow down the application and we don't really care - //if two threads write to this at the exact same time during first page hit. - //see: http://issues.umbraco.org/issue/U4-2059 - if (ApplicationContext.Current.OriginalRequestUrl.IsNullOrWhiteSpace()) - { - // If (HTTP and SSL not required) or (HTTPS and SSL required), use ports from request to configure OriginalRequestUrl. - // Otherwise, user may need to set baseUrl manually per http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks if non-standard ports used. - if ((!httpContext.Request.IsSecureConnection && !GlobalSettings.UseSSL) || (httpContext.Request.IsSecureConnection && GlobalSettings.UseSSL)) - { - // Use port from request. - ApplicationContext.Current.OriginalRequestUrl = string.Format("{0}:{1}{2}", httpContext.Request.ServerVariables["SERVER_NAME"], httpContext.Request.ServerVariables["SERVER_PORT"], IOHelper.ResolveUrl(SystemDirectories.Umbraco)); - } - else - { - // Omit port entirely. - ApplicationContext.Current.OriginalRequestUrl = string.Format("{0}{1}", httpContext.Request.ServerVariables["SERVER_NAME"], IOHelper.ResolveUrl(SystemDirectories.Umbraco)); - } - - LogHelper.Info("Setting OriginalRequestUrl: " + ApplicationContext.Current.OriginalRequestUrl); - } - - // do not process if client-side request + // do not process if client-side request if (httpContext.Request.Url.IsClientSideRequest()) return; diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index 6d80d8bedd..abc3425f3f 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -18,9 +18,7 @@ namespace umbraco.presentation var appContext = (ApplicationContext) sender; - //TODO: This won't always work, in load balanced scenarios ping will not work because - // this original request url will be public and not internal to the server. - var url = string.Format("http://{0}/ping.aspx", appContext.OriginalRequestUrl); + var url = appContext.UmbracoApplicationUrl + "/ping.aspx"; try { using (var wc = new WebClient()) From 1067b1e245b22501f1d525624768f2092710d165 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:41:20 +0200 Subject: [PATCH 29/45] BackgroundTaskRunner - refactor recurring tasks, improve shutdown Conflicts: src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs src/Umbraco.Web/Scheduling/LogScrubber.cs src/Umbraco.Web/Scheduling/Scheduler.cs src/Umbraco.Web/Umbraco.Web.csproj --- .../Sync/ServerEnvironmentHelper.cs | 6 +- .../Scheduling/BackgroundTaskRunnerTests.cs | 67 +++++------ .../XmlCacheFilePersister.cs | 26 +---- .../Scheduling/BackgroundTaskRunner.cs | 9 +- .../Scheduling/DelayedRecurringTaskBase.cs | 74 ------------- src/Umbraco.Web/Scheduling/KeepAlive.cs | 79 +++++++++---- .../Scheduling/LatchedBackgroundTaskBase.cs | 77 +++++++++++++ src/Umbraco.Web/Scheduling/LogScrubber.cs | 39 ++++--- .../Scheduling/RecurringTaskBase.cs | 104 ++++++++---------- .../Scheduling/ScheduledPublishing.cs | 71 ++++-------- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 35 ++---- src/Umbraco.Web/Scheduling/Scheduler.cs | 24 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 13 files changed, 282 insertions(+), 331 deletions(-) delete mode 100644 src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs create mode 100644 src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs index 120326e3e2..f3b241a5e0 100644 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Sync if (url.IsNullOrWhiteSpace() == false) { appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); return; } @@ -40,7 +40,7 @@ namespace Umbraco.Core.Sync var ssl = GlobalSettings.UseSSL ? "s" : ""; url = "http" + ssl + "://" + url; appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); return; } @@ -76,7 +76,7 @@ namespace Umbraco.Core.Sync IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - LogHelper.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using distributedCall/servers)"); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using distributedCall/servers)"); } } } diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 80ecc588f4..ffb186632c 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -581,15 +581,11 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } }; var task = new MyRecurringTask(runner, 200, 500); @@ -604,7 +600,10 @@ namespace Umbraco.Tests.Scheduling Assert.GreaterOrEqual(runCount, 4); // stops recurring - runner.Shutdown(false, false); + runner.Shutdown(false, true); + + // check that task has been disposed (timer has been killed, etc) + Assert.IsTrue(task.Disposed); } } @@ -651,15 +650,12 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } + }; var task = new MyDelayedRecurringTask(runner, 2000, 1000); @@ -783,37 +779,31 @@ namespace Umbraco.Tests.Scheduling } } - private class MyDelayedRecurringTask : DelayedRecurringTaskBase + private class MyDelayedRecurringTask : RecurringTaskBase { public bool HasRun { get; private set; } - public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) : base(runner, delayMilliseconds, periodMilliseconds) { } - private MyDelayedRecurringTask(MyDelayedRecurringTask source) - : base(source) - { } - public override bool IsAsync { get { return false; } } - public override void PerformRun() + public override bool PerformRun() { HasRun = true; + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } - protected override MyDelayedRecurringTask GetRecurring() - { - return new MyDelayedRecurringTask(this); - } + public override bool RunsOnShutdown { get { return true; } } } private class MyDelayedTask : ILatchedBackgroundTask @@ -867,29 +857,23 @@ namespace Umbraco.Tests.Scheduling { } } - private class MyRecurringTask : RecurringTaskBase + private class MyRecurringTask : RecurringTaskBase { private readonly int _runMilliseconds; - - public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) + public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) + : base(runner, 0, periodMilliseconds) { _runMilliseconds = runMilliseconds; } - private MyRecurringTask(MyRecurringTask source, int runMilliseconds) - : base(source) - { - _runMilliseconds = runMilliseconds; - } - - public override void PerformRun() + public override bool PerformRun() { Thread.Sleep(_runMilliseconds); + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } @@ -899,10 +883,15 @@ namespace Umbraco.Tests.Scheduling get { return false; } } - protected override MyRecurringTask GetRecurring() + public override bool RunsOnShutdown { get { return false; } } + + protected override void Dispose(bool disposing) { - return new MyRecurringTask(this, _runMilliseconds); + Disposed = true; + base.Dispose(disposing); } + + public bool Disposed { get; private set; } } private class MyTask : BaseTask diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index accdc660d7..05ee4fcd14 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -17,11 +17,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : ILatchedBackgroundTask + internal class XmlCacheFilePersister : LatchedBackgroundTaskBase { private readonly IBackgroundTaskRunner _runner; private readonly content _content; - private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); private readonly object _locko = new object(); private bool _released; private Timer _timer; @@ -38,7 +37,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 bool RunsOnShutdown { get { return true; } } + public override bool RunsOnShutdown { get { return true; } } // initialize the first instance, which is inactive (not touched yet) public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content) @@ -148,21 +147,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // if running (because of shutdown) this will have no effect // else it tells the runner it is time to run the task - _latch.Set(); + Release(); } } - public WaitHandle Latch - { - get { return _latch.WaitHandle; } - } - - public bool IsLatched - { - get { return true; } - } - - public async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { lock (_locko) { @@ -181,15 +170,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public bool IsAsync + public override bool IsAsync { get { return true; } } - public void Dispose() - { } - - public void Run() + public override void Run() { lock (_locko) { diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 575845efba..fa6efae5b3 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -385,6 +385,7 @@ namespace Umbraco.Web.Scheduling // still latched & not running on shutdown = stop here if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) { + dbgTask.Dispose(); // will not run TaskSourceCompleted(taskSource, token); return; } @@ -442,7 +443,7 @@ namespace Umbraco.Web.Scheduling try { - using (bgTask) // ensure it's disposed + try { if (bgTask.IsAsync) //configure await = false since we don't care about the context, we're on a background thread. @@ -450,6 +451,12 @@ namespace Umbraco.Web.Scheduling else bgTask.Run(); } + finally // ensure we disposed - unless latched (again) + { + var lbgTask = bgTask as ILatchedBackgroundTask; + if (lbgTask == null || lbgTask.IsLatched == false) + bgTask.Dispose(); + } } catch (Exception e) { diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs deleted file mode 100644 index af3dedbe70..0000000000 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Provides a base class for recurring background tasks. - /// - /// The type of the managed tasks. - internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, ILatchedBackgroundTask - where T : class, IBackgroundTask - { - private readonly ManualResetEventSlim _latch; - private Timer _timer; - - protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) - { - if (delayMilliseconds > 0) - { - _latch = new ManualResetEventSlim(false); - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _latch.Set(); - }); - _timer.Change(delayMilliseconds, 0); - } - } - - protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) - : base(source) - { - // no latch on recurring instances - _latch = null; - } - - public override void Run() - { - if (_latch != null) - _latch.Dispose(); - base.Run(); - } - - public override async Task RunAsync(CancellationToken token) - { - if (_latch != null) - _latch.Dispose(); - await base.RunAsync(token); - } - - public WaitHandle Latch - { - get - { - if (_latch == null) - throw new InvalidOperationException("The task is not latched."); - return _latch.WaitHandle; - } - } - - public bool IsLatched - { - get { return _latch != null && _latch.IsSet == false; } - } - - public virtual bool RunsOnShutdown - { - get { return true; } - } - } -} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index a47f8aa72c..d5ffbc811d 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -1,41 +1,76 @@ using System; using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - internal class KeepAlive + internal class KeepAlive : RecurringTaskBase { - public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings) + private readonly ApplicationContext _appContext; + + public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext) + : base(runner, delayMilliseconds, periodMilliseconds) { - using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) + _appContext = appContext; + } + + public override bool PerformRun() + { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + + string umbracoAppUrl = null; + + try { - var umbracoAppUrl = appContext.UmbracoApplicationUrl; - if (umbracoAppUrl.IsNullOrWhiteSpace()) + using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) { - LogHelper.Warn("No url for service (yet), skip."); - return; - } - - var url = umbracoAppUrl + "/ping.aspx"; - - try - { - using (var wc = new WebClient()) + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) { - wc.DownloadString(url); + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + + var url = umbracoAppUrl + "/ping.aspx"; + using (var wc = new HttpClient()) + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(url), + Method = HttpMethod.Get + }; + + var result = await wc.SendAsync(request, token); } } - catch (Exception ee) - { - LogHelper.Error( - string.Format("Error in ping. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) - , ee); - } } - + catch (Exception e) + { + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + } + + return true; // repeat + } + + public override bool IsAsync + { + get { return true; } + } + + public override bool RunsOnShutdown + { + get { return false; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs new file mode 100644 index 0000000000..c024382ee8 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + internal abstract class LatchedBackgroundTaskBase : ILatchedBackgroundTask + { + private readonly ManualResetEventSlim _latch; + private bool _disposed; + + protected LatchedBackgroundTaskBase() + { + _latch = new ManualResetEventSlim(false); + } + + /// + /// Implements IBackgroundTask.Run(). + /// + public abstract void Run(); + + /// + /// Implements IBackgroundTask.RunAsync(). + /// + public abstract Task RunAsync(CancellationToken token); + + /// + /// Indicates whether the background task can run asynchronously. + /// + public abstract bool IsAsync { get; } + + public WaitHandle Latch + { + get { return _latch.WaitHandle; } + } + + public bool IsLatched + { + get { return _latch.IsSet == false; } + } + + protected void Release() + { + _latch.Set(); + } + + protected void Reset() + { + _latch.Reset(); + } + + public abstract bool RunsOnShutdown { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // the task is going to be disposed again after execution, + // unless it is latched again, thus indicating it wants to + // remain active + + protected virtual void Dispose(bool disposing) + { + // lock on _latch instead of creating a new object as _timer is + // private, non-null, readonly - so safe here + lock (_latch) + { + if (_disposed) return; + _disposed = true; + + _latch.Dispose(); + } + } + } +} diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 79b4c0f47b..1083733003 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -7,15 +7,16 @@ using umbraco.BusinessLogic; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { - internal class LogScrubber : DelayedRecurringTaskBase + internal class LogScrubber : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -23,19 +24,7 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public LogScrubber(LogScrubber source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override LogScrubber GetRecurring() - { - return new LogScrubber(this); - } - - private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) + private static int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { int maximumAge = 24 * 60 * 60; try @@ -45,7 +34,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 hours.", e); } return maximumAge; @@ -66,15 +55,25 @@ namespace Umbraco.Web.Scheduling return interval; } - public override void PerformRun() + public override bool PerformRun() { - using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) + if (_appContext == null) return true; // repeat... + + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return false; // do NOT repeat, server status comes from config and will NOT change + } + + using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); - } + } + + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index dc82795852..3fea70a2b8 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -6,57 +6,51 @@ namespace Umbraco.Web.Scheduling /// /// Provides a base class for recurring background tasks. /// - /// The type of the managed tasks. - internal abstract class RecurringTaskBase : IBackgroundTask - where T : class, IBackgroundTask + internal abstract class RecurringTaskBase : LatchedBackgroundTaskBase { - private readonly IBackgroundTaskRunner _runner; + private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; - private Timer _timer; - private T _recurrent; + private readonly Timer _timer; + private bool _disposed; /// - /// Initializes a new instance of the class with a tasks runner and a period. + /// Initializes a new instance of the class. /// /// The task runner. + /// The delay. /// The period. /// The task will repeat itself periodically. Use this constructor to create a new task. - protected RecurringTaskBase(IBackgroundTaskRunner runner, int periodMilliseconds) + protected RecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) { _runner = runner; _periodMilliseconds = periodMilliseconds; - } - /// - /// Initializes a new instance of the class with a source task. - /// - /// The source task. - /// Use this constructor to create a new task from a source task in GetRecurring. - protected RecurringTaskBase(RecurringTaskBase source) - { - _runner = source._runner; - _timer = source._timer; - _periodMilliseconds = source._periodMilliseconds; + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + + _timer = new Timer(_ => Release()); + _timer.Change(delayMilliseconds, 0); } /// /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual void Run() + public override void Run() { - PerformRun(); - Repeat(); + var shouldRepeat = PerformRun(); + if (shouldRepeat) Repeat(); } /// /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { - await PerformRunAsync(token); - Repeat(); + var shouldRepeat = await PerformRunAsync(token); + if (shouldRepeat) Repeat(); } private void Repeat() @@ -64,53 +58,45 @@ namespace Umbraco.Web.Scheduling // again? if (_runner.IsCompleted) return; // fail fast - if (_periodMilliseconds == 0) return; + if (_periodMilliseconds == 0) return; // safe - _recurrent = GetRecurring(); - if (_recurrent == null) - { - _timer.Dispose(); - _timer = null; - return; // done - } + Reset(); // re-latch - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent)); - _timer.Change(_periodMilliseconds, 0); + // try to add again (may fail if runner has completed) + // if added, re-start the timer, else kill it + if (_runner.TryAdd(this)) + _timer.Change(_periodMilliseconds, 0); + else + Dispose(true); } - /// - /// Indicates whether the background task can run asynchronously. - /// - public abstract bool IsAsync { get; } - /// /// Runs the background task. /// - public abstract void PerformRun(); + /// A value indicating whether to repeat the task. + public abstract bool PerformRun(); /// /// Runs the task asynchronously. /// /// A cancellation token. - /// A instance representing the execution of the background task. - public abstract Task PerformRunAsync(CancellationToken token); + /// A instance representing the execution of the background task, + /// and returning a value indicating whether to repeat the task. + public abstract Task PerformRunAsync(CancellationToken token); - /// - /// Gets a new occurence of the recurring task. - /// - /// A new task instance to be queued, or null to terminate the recurring task. - /// The new task instance must be created via the RecurringTaskBase(RecurringTaskBase{T} source) constructor, - /// where source is the current task, eg: return new MyTask(this); - protected abstract T GetRecurring(); + protected override void Dispose(bool disposing) + { + // 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; - /// - /// Dispose the task. - /// - public virtual void 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 d1dc8d1935..78a91f6341 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -10,14 +10,12 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing : DelayedRecurringTaskBase + internal class ScheduledPublishing : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private static bool _isPublishingRunning; - - public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -25,48 +23,34 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - private ScheduledPublishing(ScheduledPublishing source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledPublishing GetRecurring() - { - return new ScheduledPublishing(this); - } - - public override void PerformRun() + public override bool PerformRun() { throw new NotImplementedException(); } - public override async Task PerformRunAsync(CancellationToken token) - { - - if (_appContext == null) return; + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { LogHelper.Debug("Does not run on slave servers."); - return; + return false; // do NOT repeat, server status comes from config and will NOT change } using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - - var umbracoAppUrl = _appContext.UmbracoApplicationUrl; - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - LogHelper.Warn("No url for service (yet), skip."); - return; - } + string umbracoAppUrl = null; try { + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; using (var wc = new HttpClient()) { @@ -79,27 +63,16 @@ namespace Umbraco.Web.Scheduling //pass custom the authorization header request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - try - { - var result = await wc.SendAsync(request, token); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling scheduled publish url", ex); - } + var result = await wc.SendAsync(request, token); } } - catch (Exception ee) + catch (Exception e) { - LogHelper.Error( - string.Format("An error occurred with the scheduled publishing. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) - , ee); + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); } - finally - { - _isPublishingRunning = false; - } - } + } + + return true; // repeat } public override bool IsAsync diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 1015b2d4f6..92214e5199 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -15,14 +15,13 @@ namespace Umbraco.Web.Scheduling // would need to be a publicly available task (URL) which isn't really very good :( // We should really be using the AdminTokenAuthorizeAttribute for this stuff - internal class ScheduledTasks : DelayedRecurringTaskBase + internal class ScheduledTasks : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); - private static bool _isPublishingRunning = false; - public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -30,18 +29,6 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public ScheduledTasks(ScheduledTasks source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledTasks GetRecurring() - { - return new ScheduledTasks(this); - } - private async Task ProcessTasksAsync(CancellationToken token) { var scheduledTasks = _settings.ScheduledTasks.Tasks; @@ -99,25 +86,23 @@ namespace Umbraco.Web.Scheduling } } - public override void PerformRun() + public override bool PerformRun() { throw new NotImplementedException(); } - public override async Task PerformRunAsync(CancellationToken token) + public override async Task PerformRunAsync(CancellationToken token) { + if (_appContext == null) return true; // repeat... + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { LogHelper.Debug("Does not run on slave servers."); - return; + return false; // do NOT repeat, server status comes from config and will NOT change } using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - try { await ProcessTasksAsync(token); @@ -126,11 +111,9 @@ namespace Umbraco.Web.Scheduling { LogHelper.Error("Error executing scheduled task", ee); } - finally - { - _isPublishingRunning = false; - } } + + return true; // repeat } public override bool IsAsync diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 409a67028f..8831a81838 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,11 +1,7 @@ -using System; -using System.Threading; -using System.Web; +using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -18,7 +14,7 @@ namespace Umbraco.Web.Scheduling /// internal sealed class Scheduler : ApplicationEventHandler { - private static Timer _pingTimer; + private static BackgroundTaskRunner _keepAliveRunner; private static BackgroundTaskRunner _publishingRunner; private static BackgroundTaskRunner _tasksRunner; private static BackgroundTaskRunner _scrubberRunner; @@ -48,30 +44,24 @@ namespace Umbraco.Web.Scheduling LogHelper.Debug(() => "Initializing the scheduler"); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _keepAliveRunner = new BackgroundTaskRunner("KeepAlive"); _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing"); _tasksRunner = new BackgroundTaskRunner("ScheduledTasks"); _scrubberRunner = new BackgroundTaskRunner("LogScrubber"); var settings = UmbracoConfig.For.UmbracoSettings(); - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // also make the timer static to ensure further GC safety - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - // ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies - _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _pingTimer.Change(60000, 300000); + // ping/keepalive + // on all servers + _keepAliveRunner.Add(new KeepAlive(_keepAliveRunner, 60000, 300000, applicationContext)); // scheduled publishing/unpublishing // install on all, will only run on non-slaves servers - // both are delayed recurring tasks _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); // log scrubbing - // install & run on all servers - // LogScrubber is a delayed recurring task + // install on all, will only run on non-slaves servers _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d6ed99c4cd..7c0482b4ed 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -268,6 +268,7 @@ + @@ -500,7 +501,6 @@ - From a277ac2fe4f9c296aa9eb850698111dc8c4a227c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 17:55:08 +0200 Subject: [PATCH 30/45] udpates some logging stuff which will also make logging faster. --- src/Umbraco.Core/CoreBootManager.cs | 4 +++- src/Umbraco.Core/Logging/LogHelper.cs | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index c6591b7e47..f5dbdcf957 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -66,7 +66,9 @@ namespace Umbraco.Core InitializeProfilerResolver(); - _timer = DisposableTimer.TraceDuration("Umbraco application starting on '" + NetworkHelper.MachineName + "'", "Umbraco application startup complete"); + _timer = DisposableTimer.TraceDuration( + string.Format("Umbraco {0} application starting on {1}", UmbracoVersion.Current, NetworkHelper.MachineName), + "Umbraco application startup complete"); CreateApplicationCache(); diff --git a/src/Umbraco.Core/Logging/LogHelper.cs b/src/Umbraco.Core/Logging/LogHelper.cs index 14aab56f0b..05b9e7ed65 100644 --- a/src/Umbraco.Core/Logging/LogHelper.cs +++ b/src/Umbraco.Core/Logging/LogHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Web; @@ -40,8 +41,11 @@ namespace Umbraco.Core.Logging /// private static string PrefixThreadId(string generateMessageFormat) { - return "[T" + Thread.CurrentThread.ManagedThreadId + "/D" + AppDomain.CurrentDomain.Id + "] " + generateMessageFormat; - } + return (_prefixThreadId ?? (_prefixThreadId = "[P" + Process.GetCurrentProcess().Id + "/T" + Thread.CurrentThread.ManagedThreadId + "/D" + AppDomain.CurrentDomain.Id + "] ")) + + generateMessageFormat; + } + + private static string _prefixThreadId = null; #region Error /// From e69422c8d8435c64a87d9e7da27b99ec1ca547b8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2015 18:14:31 +0200 Subject: [PATCH 31/45] Fixes tests (backports cache fix by name) --- .../Cache/DictionaryCacheProviderBase.cs | 18 +++++++-------- .../Cache/HttpRequestCacheProvider.cs | 4 ++-- .../Cache/HttpRuntimeCacheProvider.cs | 6 +++-- .../Cache/ObjectCacheRuntimeCacheProvider.cs | 18 ++++++++------- src/Umbraco.Core/TypeFinder.cs | 22 +++++++++++++------ 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs index 543609dd2e..a6275eca7a 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs @@ -90,7 +90,7 @@ namespace Umbraco.Core.Cache { foreach (var entry in GetDictionaryEntries() .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -105,7 +105,7 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(string typeName) { - var type = Type.GetType(typeName); + var type = TypeFinder.GetTypeByName(typeName); if (type == null) return; var isInterface = type.IsInterface; using (WriteLock) @@ -123,7 +123,7 @@ namespace Umbraco.Core.Cache return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); }) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -147,7 +147,7 @@ namespace Umbraco.Core.Cache return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); }) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -171,10 +171,10 @@ namespace Umbraco.Core.Cache // if T is an interface remove anything that implements that interface // otherwise remove exact types (not inherited types) return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - // run predicate on the 'public key' part only, ie without prefix - && predicate(((string) x.Key).Substring(plen), (T) value); + // run predicate on the 'public key' part only, ie without prefix + && predicate(((string)x.Key).Substring(plen), (T)value); })) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -186,7 +186,7 @@ namespace Umbraco.Core.Cache foreach (var entry in GetDictionaryEntries() .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -198,7 +198,7 @@ namespace Umbraco.Core.Cache foreach (var entry in GetDictionaryEntries() .Where(x => Regex.IsMatch(((string)x.Key).Substring(plen), regexString)) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } 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 dcd5198eec..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) @@ -140,6 +140,7 @@ namespace Umbraco.Core.Cache var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); lck.UpgradeToWriteLock(); + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } } @@ -190,13 +191,14 @@ 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); using (new WriteLock(_locker)) { + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } } diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index 197fdce7f7..045bc4ff4e 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -49,12 +49,12 @@ namespace Umbraco.Core.Cache { if (MemoryCache[key] == null) return; MemoryCache.Remove(key); - } + } } public virtual void ClearCacheObjectTypes(string typeName) { - var type = Type.GetType(typeName); + var type = TypeFinder.GetTypeByName(typeName); if (type == null) return; var isInterface = type.IsInterface; using (new WriteLock(_locker)) @@ -81,7 +81,7 @@ namespace Umbraco.Core.Cache { using (new WriteLock(_locker)) { - var typeOfT = typeof (T); + var typeOfT = typeof(T); var isInterface = typeOfT.IsInterface; foreach (var key in MemoryCache .Where(x => @@ -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 @@ -216,6 +216,7 @@ namespace Umbraco.Core.Cache var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); lck.UpgradeToWriteLock(); + //NOTE: This does an add or update MemoryCache.Set(cacheKey, result, policy); } } @@ -242,6 +243,7 @@ namespace Umbraco.Core.Cache if (value == null) return; // do not store null values (backward compat) var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); + //NOTE: This does an add or update MemoryCache.Set(cacheKey, result, policy); } @@ -262,7 +264,7 @@ namespace Umbraco.Core.Cache { policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); } - + if (removedCallback != null) { policy.RemovedCallback = arguments => @@ -293,6 +295,6 @@ namespace Umbraco.Core.Cache } return policy; } - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index 6b257fecfe..962f0e3d91 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,7 +700,14 @@ namespace Umbraco.Core #endregion - + public static Type GetTypeByName(string typeName) + { + var type = Type.GetType(typeName); + if (type != null) return type; + return AppDomain.CurrentDomain.GetAssemblies() + .Select(x => x.GetType(typeName)) + .FirstOrDefault(x => x != null); + } } } From 1d00e3c5d8c1b4f4b62ac25410fc51010c9451d7 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 8 Jul 2015 21:28:59 +0200 Subject: [PATCH 32/45] UmbracoApplication - log unhandled exceptions Conflicts: src/Umbraco.Core/UmbracoApplicationBase.cs --- src/Umbraco.Core/UmbracoApplicationBase.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index 07e3a6bbf3..550228e3a8 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -36,6 +36,19 @@ namespace Umbraco.Core //don't output the MVC version header (security) MvcHandler.DisableMvcResponseHeader = true; + //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() From 6d50bd891a9d379ae9865072d3ac909c272f3300 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2015 14:30:32 +0200 Subject: [PATCH 33/45] moves the ApplicationContext.IsConfigured to a lazy initializer --- src/Umbraco.Core/ApplicationContext.cs | 59 ++++++++++---------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index c08cb0e353..0201a03708 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -37,6 +37,8 @@ namespace Umbraco.Core _databaseContext = dbContext; _services = serviceContext; ApplicationCache = cache; + + Init(); } /// @@ -46,6 +48,8 @@ namespace Umbraco.Core public ApplicationContext(CacheHelper cache) { ApplicationCache = cache; + + Init(); } /// @@ -142,16 +146,10 @@ namespace Umbraco.Core // GlobalSettings.CurrentVersion returns the hard-coded "current version" // the system is configured if they match // if they don't, install runs, updates web.config (presumably) and updates GlobalSettings.ConfiguredStatus - // - // then there is Application["umbracoNeedConfiguration"] which makes no sense... getting rid of it... SD: I have actually remove that now! - // + public bool IsConfigured { - // todo - we should not do this - ok for now - get - { - return Configured; - } + get { return _configured.Value; } } /// @@ -196,37 +194,24 @@ namespace Umbraco.Core } internal string _umbracoApplicationUrl; // internal for tests - private bool _versionsDifferenceReported; - /// - /// Checks if the version configured matches the assembly version - /// - private bool Configured - { - get - { - try - { - var configStatus = ConfigurationStatus; - var currentVersion = UmbracoVersion.Current.ToString(3); - var ok = configStatus == currentVersion; + private Lazy _configured; - if (ok == false && _versionsDifferenceReported == false) - { - // remember it's been reported so we don't flood the log - // no thread-safety so there may be a few log entries, doesn't matter - _versionsDifferenceReported = true; - LogHelper.Info("CurrentVersion different from configStatus: '" + currentVersion + "','" + configStatus + "'"); - } - - return ok; - } - catch - { - return false; - } - } - } + private void Init() + { + //Create the lazy value to resolve whether or not the application is 'configured' + _configured = new Lazy(() => + { + var configStatus = ConfigurationStatus; + var currentVersion = UmbracoVersion.Current.ToString(3); + var ok = configStatus == currentVersion; + if (ok == false) + { + LogHelper.Debug("CurrentVersion different from configStatus: '" + currentVersion + "','" + configStatus + "'"); + } + return ok; + }); + } private string ConfigurationStatus { From 431d066300b11eac002eb716c0d7a7388058da4d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2015 14:31:59 +0200 Subject: [PATCH 34/45] Changes the ApplicationEventHandler to check if the db is configured instead of being connected to which can cause issues if SQLCE file is locked by another app domain on startup. Then we verify in the CoreBootManager that the db can be connected to with a retry policy. Changes the db context CanConnect method to not store a static value since it might later be able to be connected to. --- src/Umbraco.Core/ApplicationEventHandler.cs | 4 +-- src/Umbraco.Core/CoreBootManager.cs | 31 +++++++++++++++++++ src/Umbraco.Core/DatabaseContext.cs | 20 ++---------- .../UmbracoStartupFailedException.cs | 18 +++++++++++ src/Umbraco.Core/TypeFinder.cs | 6 +++- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 6 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs 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/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index f5dbdcf957..c5d3e2a4c7 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; @@ -261,6 +263,9 @@ namespace Umbraco.Core FreezeResolution(); + //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")) @@ -299,6 +304,32 @@ namespace Umbraco.Core 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."); + } + + } + /// /// Freeze resolution to not allow Resolvers to be modified /// diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index fad96b849d..dfaebb0315 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -26,8 +26,6 @@ namespace Umbraco.Core { private readonly IDatabaseFactory _factory; private bool _configured; - private bool _canConnect; - private volatile bool _connectCheck = false; private readonly object _locker = new object(); private string _connectionString; private string _providerName; @@ -67,21 +65,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/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index 962f0e3d91..abaf5ae5e0 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -700,7 +700,11 @@ namespace Umbraco.Core #endregion - public static Type GetTypeByName(string typeName) + //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. + internal static Type GetTypeByName(string typeName) { var type = Type.GetType(typeName); if (type != null) return type; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 67ce55b8f1..4e155e1b3d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -312,6 +312,7 @@ + From da9c810d78470f671ce5fed9ee203887302072c3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2015 14:32:50 +0200 Subject: [PATCH 35/45] Updates examine initialization of its IIndexCriteria to be lazy, this will save a few db calls on startup and ensure they are excuted at the very last possible time. --- .../Config/IndexSetExtensions.cs | 58 +--------- .../Config/LazyIndexCriteria.cs | 106 ++++++++++++++++++ src/UmbracoExamine/UmbracoExamine.csproj | 1 + 3 files changed, 109 insertions(+), 56 deletions(-) create mode 100644 src/UmbracoExamine/Config/LazyIndexCriteria.cs 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 @@ + From bce8affe163c5900eb8a8f442b2072d59e0dc306 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2015 15:19:47 +0200 Subject: [PATCH 36/45] removes unused method --- src/Umbraco.Core/ApplicationContext.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 0201a03708..0f160d5c83 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -228,12 +228,6 @@ namespace Umbraco.Core } } - private void AssertIsReady() - { - if (!this.IsReady) - throw new Exception("ApplicationContext is not ready yet."); - } - private void AssertIsNotReady() { if (this.IsReady) From 82a8404b2ea8a41ed56fb467e8fe162b3fb9bc17 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2015 18:30:52 +0200 Subject: [PATCH 37/45] Updates TemplateRenderer to not use singletons and to ensure that the http context values are copied back if anything fails --- src/Umbraco.Web/Templates/TemplateRenderer.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) 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) From 0f56fa4baa6ce3c6bca7c46d3bda2541895cee25 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 13 Jul 2015 14:08:50 +0200 Subject: [PATCH 38/45] Bugfix content xml doctype issue --- .../config/ClientDependency.config | 2 +- .../umbraco.presentation/content.cs | 37 +++++++++++++------ .../umbraco/preview/PreviewContent.cs | 4 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 1c16a6e029..0f6c04a6b7 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - +