diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/DisabledModelsBuilderComponent.cs deleted file mode 100644 index f7fab098b0..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/DisabledModelsBuilderComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Umbraco.Core.Composing; -using Umbraco.ModelsBuilder.Embedded.BackOffice; -using Umbraco.Web.Features; - -namespace Umbraco.ModelsBuilder.Embedded.Compose -{ - // TODO: This needs to die, see TODO in ModelsBuilderComposer. This is also no longer used in this netcore - // codebase. Potentially this could be changed to ext methods if necessary that could be used by end users who will - // install the community MB package to disable any built in MB stuff. - - /// - /// Special component used for when MB is disabled with the legacy MB is detected - /// - public sealed class DisabledModelsBuilderComponent : IComponent - { - private readonly UmbracoFeatures _features; - - public DisabledModelsBuilderComponent(UmbracoFeatures features) - { - _features = features; - } - - public void Initialize() - { - //disable the embedded dashboard controller - _features.Disabled.Controllers.Add(); - } - - public void Terminate() - { - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index b5997b8419..dddef525ff 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -9,15 +9,15 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Extensions.Logging; -using Umbraco.Core.Configuration; +using Microsoft.Extensions.Options; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.ModelsBuilder.Embedded.Building; using File = System.IO.File; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; namespace Umbraco.ModelsBuilder.Embedded { @@ -30,21 +30,21 @@ namespace Umbraco.ModelsBuilder.Embedded private readonly IProfilingLogger _profilingLogger; private readonly ILogger _logger; private readonly FileSystemWatcher _watcher; - private int _ver, _skipver; + private int _ver; + private int _skipver; private readonly int _debugLevel; private RoslynCompiler _roslynCompiler; private UmbracoAssemblyLoadContext _currentAssemblyLoadContext; private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( - private UmbracoServices UmbracoServices => _umbracoServices.Value; - - private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); - private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; - + private static readonly Regex s_assemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); + private static readonly string[] s_ourFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; private readonly ModelsBuilderSettings _config; private readonly IHostingEnvironment _hostingEnvironment; private readonly IApplicationShutdownRegistry _hostingLifetime; private readonly ModelsGenerationError _errors; private readonly IPublishedValueFallback _publishedValueFallback; + private static readonly Regex s_usingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex s_aattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); public PureLiveModelFactory( Lazy umbracoServices, @@ -65,11 +65,16 @@ namespace Umbraco.ModelsBuilder.Embedded _errors = new ModelsGenerationError(config, _hostingEnvironment); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip - if (!hostingEnvironment.IsHosted) return; + if (!hostingEnvironment.IsHosted) + { + return; + } var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } // BEWARE! if the watcher is not properly released then for some reason the // BuildManager will start confusing types - using a 'registered object' here @@ -83,11 +88,29 @@ namespace Umbraco.ModelsBuilder.Embedded _debugLevel = _config.DebugLevel; } - #region ILivePublishedModelFactory + private UmbracoServices UmbracoServices => _umbracoServices.Value; /// public object SyncRoot { get; } = new object(); + // gets the RoslynCompiler + private RoslynCompiler RoslynCompiler + { + get + { + if (_roslynCompiler != null) + { + return _roslynCompiler; + } + + _roslynCompiler = new RoslynCompiler(System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies)); + return _roslynCompiler; + } + } + + /// + public bool Enabled => _config.Enable; + /// public void Refresh() { @@ -95,16 +118,14 @@ namespace Umbraco.ModelsBuilder.Embedded EnsureModels(); } - #endregion - - #region IPublishedModelFactory - public IPublishedElement CreateModel(IPublishedElement element) { // get models, rebuilding them if needed - var infos = EnsureModels()?.ModelInfos; + Dictionary infos = EnsureModels()?.ModelInfos; if (infos == null) + { return element; + } // be case-insensitive var contentTypeAlias = element.ContentType.Alias; @@ -120,7 +141,7 @@ namespace Umbraco.ModelsBuilder.Embedded // NOT when building models public Type MapModelType(Type type) { - var infos = EnsureModels(); + Infos infos = EnsureModels(); return ModelType.Map(type, infos.ModelTypeMap); } @@ -128,34 +149,39 @@ namespace Umbraco.ModelsBuilder.Embedded // NOT when building models public IList CreateModelList(string alias) { - var infos = EnsureModels(); + Infos infos = EnsureModels(); // fail fast if (infos == null) + { return new List(); + } - if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo)) + if (!infos.ModelInfos.TryGetValue(alias, out ModelInfo modelInfo)) + { return new List(); + } - var ctor = modelInfo.ListCtor; - if (ctor != null) return ctor(); + Func ctor = modelInfo.ListCtor; + if (ctor != null) + { + return ctor(); + } - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); return ctor(); } - /// - public bool Enabled => _config.Enable; - /// public void Reset() { if (_config.Enable) + { ResetModels(); + } } - #endregion #region Compilation // deadlock note @@ -229,30 +255,30 @@ namespace Umbraco.ModelsBuilder.Embedded var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } // clear stuff var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); - if (File.Exists(dllPathFile)) File.Delete(dllPathFile); - if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + if (File.Exists(dllPathFile)) + { + File.Delete(dllPathFile); + } + + if (File.Exists(modelsHashFile)) + { + File.Delete(modelsHashFile); + } } finally { if (_locker.IsWriteLockHeld) + { _locker.ExitWriteLock(); - } - } - - // gets the RoslynCompiler - private RoslynCompiler RoslynCompiler - { - get - { - if (_roslynCompiler != null) return _roslynCompiler; - _roslynCompiler = new RoslynCompiler(System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies)); - return _roslynCompiler; + } } } @@ -260,31 +286,42 @@ namespace Umbraco.ModelsBuilder.Embedded internal Infos EnsureModels() { if (_debugLevel > 0) + { _logger.LogDebug("Ensuring models."); + } // don't use an upgradeable lock here because only 1 thread at a time could enter it try { _locker.EnterReadLock(); if (_hasModels) + { return _infos; + } } finally { if (_locker.IsReadLockHeld) + { _locker.ExitReadLock(); + } } var roslynLocked = false; try { + // TODO: DO NOT LOCK ON RoslynCompiler + // always take the BuildManager lock *before* taking the _locker lock // to avoid possible deadlock situations (see notes above) Monitor.Enter(RoslynCompiler, ref roslynLocked); _locker.EnterUpgradeableReadLock(); - if (_hasModels) return _infos; + if (_hasModels) + { + return _infos; + } _locker.EnterWriteLock(); @@ -296,14 +333,13 @@ namespace Umbraco.ModelsBuilder.Embedded { try { - var assembly = GetModelsAssembly(_pendingRebuild); + Assembly assembly = GetModelsAssembly(_pendingRebuild); // the one below can be used to simulate an issue with BuildManager, ie it will register // the models with the factory but NOT with the BuildManager, which will not recompile views. // this is for U4-8043 which is an obvious issue but I cannot replicate - //_modelsAssembly = _modelsAssembly ?? assembly; - - var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); + // _modelsAssembly = _modelsAssembly ?? assembly; + IEnumerable types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); _errors.Clear(); } @@ -330,18 +366,26 @@ namespace Umbraco.ModelsBuilder.Embedded finally { if (_locker.IsWriteLockHeld) + { _locker.ExitWriteLock(); + } + if (_locker.IsUpgradeableReadLockHeld) + { _locker.ExitUpgradeableReadLock(); + } + if (roslynLocked) + { Monitor.Exit(RoslynCompiler); + } } } private Assembly ReloadAssembly(string pathToAssembly) { // If there's a current AssemblyLoadContext, unload it before creating a new one. - if(!(_currentAssemblyLoadContext is null)) + if (!(_currentAssemblyLoadContext is null)) { _currentAssemblyLoadContext.Unload(); GC.Collect(); @@ -364,9 +408,11 @@ namespace Umbraco.ModelsBuilder.Embedded { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } - var typeModels = UmbracoServices.GetAllTypes(); + IList typeModels = UmbracoServices.GetAllTypes(); var currentHash = TypeModelHasher.Hash(typeModels); var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); @@ -375,7 +421,6 @@ namespace Umbraco.ModelsBuilder.Embedded // caching the generated models speeds up booting // currentHash hashes both the types & the user's partials - if (!forceRebuild) { _logger.LogDebug("Looking for cached models."); @@ -415,7 +460,7 @@ namespace Umbraco.ModelsBuilder.Embedded { assembly = ReloadAssembly(dllPath); - var attr = assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute(); if (attr != null && attr.PureLive && attr.SourceHash == currentHash) { // if we were to resume at that revision, then _ver would keep increasing @@ -431,17 +476,23 @@ namespace Umbraco.ModelsBuilder.Embedded _logger.LogDebug("Cached models dll cannot be loaded (invalid assembly)."); } else if (!File.Exists(dllPath)) + { _logger.LogDebug("Cached models dll does not exist."); + } else if (File.Exists(dllPath + ".delete")) + { _logger.LogDebug("Cached models dll is marked for deletion."); + } else + { _logger.LogDebug("Cached models dll cannot be loaded (why?)."); + } } // must reset the version in the file else it would keep growing // loading cached modules only happens when the app restarts var text = File.ReadAllText(projFile); - var match = AssemblyVersionRegex.Match(text); + Match match = s_assemblyVersionRegex.Match(text); if (match.Success) { text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")"); @@ -478,8 +529,9 @@ namespace Umbraco.ModelsBuilder.Embedded // AssemblyVersion is so that we have a different version for each rebuild var ver = _ver == _skipver ? ++_ver : _ver; _ver++; - code = code.Replace("//ASSATTR", $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] -[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); + string mbAssemblyDirective = $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] +[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"; + code = code.Replace("//ASSATTR", mbAssemblyDirective); File.WriteAllText(modelsSrcFile, code); // generate proj, save @@ -515,21 +567,20 @@ namespace Umbraco.ModelsBuilder.Embedded if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); - var dirInfo = new DirectoryInfo(dllPath).Parent; - var files = dirInfo.GetFiles().Where(f => f.FullName != dllPath); - foreach(var file in files) + DirectoryInfo dirInfo = new DirectoryInfo(dllPath).Parent; + IEnumerable files = dirInfo.GetFiles().Where(f => f.FullName != dllPath); + foreach (FileInfo file in files) { try { File.Delete(file.FullName); } - catch(UnauthorizedAccessException) + catch (UnauthorizedAccessException) { // The file is in use, we'll try again next time... // This shouldn't happen anymore. } } - } } @@ -537,7 +588,10 @@ namespace Umbraco.ModelsBuilder.Embedded { var dirInfo = new DirectoryInfo(Path.Combine(_config.ModelsDirectoryAbsolute(_hostingEnvironment), "Compiled")); if (!dirInfo.Exists) + { Directory.CreateDirectory(dirInfo.FullName); + } + return Path.Combine(dirInfo.FullName, $"generated.cs{currentHash}.dll"); } @@ -551,51 +605,69 @@ namespace Umbraco.ModelsBuilder.Embedded // useful to have the source around for debugging. try { - if (File.Exists(dllPathFile)) File.Delete(dllPathFile); - if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); - if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now); + if (File.Exists(dllPathFile)) + { + File.Delete(dllPathFile); + } + + if (File.Exists(modelsHashFile)) + { + File.Delete(modelsHashFile); + } + + if (File.Exists(projFile)) + { + File.SetLastWriteTime(projFile, DateTime.Now); + } } catch { /* enough */ } } private static Infos RegisterModels(IEnumerable types) { - var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) }; + Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var map = new Dictionary(); - foreach (var type in types) + foreach (Type type in types) { ConstructorInfo constructor = null; Type parameterType = null; - foreach (var ctor in type.GetConstructors()) + foreach (ConstructorInfo ctor in type.GetConstructors()) { - var parms = ctor.GetParameters(); + ParameterInfo[] parms = ctor.GetParameters(); if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { if (constructor != null) + { throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); + } + constructor = ctor; parameterType = parms[0].ParameterType; } } if (constructor == null) + { throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet."); + } - var attribute = type.GetCustomAttribute(false); + PublishedModelAttribute attribute = type.GetCustomAttribute(false); var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; if (modelInfos.TryGetValue(typeName, out var modelInfo)) + { throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); + } // TODO: use Core's ReflectionUtilities.EmitCtor !! // Yes .. DynamicMethod is uber slow // TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still? // See CtorInvokeBenchmarks var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); - var gen = meth.GetILGenerator(); + ILGenerator gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Newobj, constructor); @@ -613,10 +685,14 @@ namespace Umbraco.ModelsBuilder.Embedded { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) + { File.Delete(file); + } var builder = new TextBuilder(_config, typeModels); @@ -627,9 +703,6 @@ namespace Umbraco.ModelsBuilder.Embedded return code; } - private static readonly Regex UsingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); - private static readonly Regex AattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); - private static string GenerateModelsProj(IDictionary files) { // ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj @@ -637,21 +710,25 @@ namespace Umbraco.ModelsBuilder.Embedded // group all 'using' at the top of the file (else fails) var usings = new List(); - foreach (var k in files.Keys.ToList()) - files[k] = UsingRegex.Replace(files[k], m => + foreach (string k in files.Keys.ToList()) + { + files[k] = s_usingRegex.Replace(files[k], m => { usings.Add(m.Groups[1].Value); return string.Empty; }); + } // group all '[assembly:...]' at the top of the file (else fails) var aattrs = new List(); - foreach (var k in files.Keys.ToList()) - files[k] = AattrRegex.Replace(files[k], m => + foreach (string k in files.Keys.ToList()) + { + files[k] = s_aattrRegex.Replace(files[k], m => { aattrs.Add(m.Groups[1].Value); return string.Empty; }); + } var text = new StringBuilder(); foreach (var u in usings.Distinct()) @@ -660,14 +737,16 @@ namespace Umbraco.ModelsBuilder.Embedded text.Append(u); text.Append(";\r\n"); } + foreach (var a in aattrs) { text.Append("[assembly:"); text.Append(a); text.Append("]\r\n"); } + text.Append("\r\n\r\n"); - foreach (var f in files) + foreach (KeyValuePair f in files) { text.Append("// FILE: "); text.Append(f.Key); @@ -675,29 +754,12 @@ namespace Umbraco.ModelsBuilder.Embedded text.Append(f.Value); text.Append("\r\n\r\n\r\n"); } + text.Append("// EOF\r\n"); return text.ToString(); } - internal class Infos - { - public Dictionary ModelTypeMap { get; set; } - public Dictionary ModelInfos { get; set; } - } - - internal class ModelInfo - { - public Type ParameterType { get; set; } - public Func Ctor { get; set; } - public Type ModelType { get; set; } - public Func ListCtor { get; set; } - } - - #endregion - - #region Watching - private void WatcherOnChanged(object sender, FileSystemEventArgs args) { var changed = args.Name; @@ -715,7 +777,7 @@ namespace Umbraco.ModelsBuilder.Embedded //} // always ignore our own file changes - if (OurFiles.Contains(changed)) + if (s_ourFiles.Contains(changed)) return; _logger.LogInformation("Detected files changes."); @@ -731,6 +793,24 @@ namespace Umbraco.ModelsBuilder.Embedded _hostingLifetime.UnregisterObject(this); } + internal class Infos + { + public Dictionary ModelTypeMap { get; set; } + + public Dictionary ModelInfos { get; set; } + } + + internal class ModelInfo + { + public Type ParameterType { get; set; } + + public Func Ctor { get; set; } + + public Type ModelType { get; set; } + + public Func ListCtor { get; set; } + } + #endregion } } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs index e2a7c35daa..4993b68568 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Common.Localization return NullProviderCultureResult; } - lock(_locker) + lock (_locker) { // We need to dynamically change the supported cultures since we won't ever know what languages are used since // they are dynamic within Umbraco.