PureLiveModelFactory linting cleanup

This commit is contained in:
Shannon
2021-01-13 15:18:59 +11:00
parent 6fbe515adb
commit 2201a5a590
3 changed files with 174 additions and 127 deletions

View File

@@ -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.
/// <summary>
/// Special component used for when MB is disabled with the legacy MB is detected
/// </summary>
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<ModelsBuilderDashboardController>();
}
public void Terminate()
{
}
}
}

View File

@@ -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<PureLiveModelFactory> _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> _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> 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;
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public bool Enabled => _config.Enable;
/// <inheritdoc />
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<string, ModelInfo> 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<IPublishedElement>();
}
if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo))
if (!infos.ModelInfos.TryGetValue(alias, out ModelInfo modelInfo))
{
return new List<IPublishedElement>();
}
var ctor = modelInfo.ListCtor;
if (ctor != null) return ctor();
Func<IList> 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<Func<IList>>(declaring: listType);
return ctor();
}
/// <inheritdoc />
public bool Enabled => _config.Enable;
/// <inheritdoc />
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<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
// _modelsAssembly = _modelsAssembly ?? assembly;
IEnumerable<Type> types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
_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<TypeModel> 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>();
ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
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<FileInfo> 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<Type> types)
{
var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
var map = new Dictionary<string, Type>();
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<PublishedModelAttribute>(false);
PublishedModelAttribute attribute = type.GetCustomAttribute<PublishedModelAttribute>(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<string, string> 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<string>();
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<string>();
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<string, string> 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<string, Type> ModelTypeMap { get; set; }
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
}
internal class ModelInfo
{
public Type ParameterType { get; set; }
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
public Type ModelType { get; set; }
public Func<IList> 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<string, Type> ModelTypeMap { get; set; }
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
}
internal class ModelInfo
{
public Type ParameterType { get; set; }
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
public Type ModelType { get; set; }
public Func<IList> ListCtor { get; set; }
}
#endregion
}
}

View File

@@ -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.