Merge remote-tracking branch 'origin/netcore/netcore' into feature/8651-config-options-patten
# Conflicts: # src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs # src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs # src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs # src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs # src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs # src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs # src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs # src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs # src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs
This commit is contained in:
@@ -8,13 +8,9 @@ using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using System.Web.Compilation;
|
||||
using System.Web.WebPages.Razor;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.ModelsBuilder.Embedded.Building;
|
||||
@@ -26,7 +22,6 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
{
|
||||
internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject
|
||||
{
|
||||
private Assembly _modelsAssembly;
|
||||
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
|
||||
private volatile bool _hasModels; // volatile 'cos reading outside lock
|
||||
@@ -35,18 +30,19 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
private int _ver, _skipver;
|
||||
private readonly int _debugLevel;
|
||||
private BuildManager _theBuildManager;
|
||||
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 const string ProjVirt = "~/App_Data/Models/all.generated.cs";
|
||||
private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" };
|
||||
private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" };
|
||||
|
||||
private readonly ModelsBuilderConfig _config;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IApplicationShutdownRegistry _hostingLifetime;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly ModelsGenerationError _errors;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
|
||||
public PureLiveModelFactory(
|
||||
Lazy<UmbracoServices> umbracoServices,
|
||||
@@ -54,22 +50,20 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
IOptions<ModelsBuilderConfig> config,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IApplicationShutdownRegistry hostingLifetime,
|
||||
IIOHelper ioHelper)
|
||||
IPublishedValueFallback publishedValueFallback)
|
||||
{
|
||||
_umbracoServices = umbracoServices;
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_hostingLifetime = hostingLifetime;
|
||||
_ioHelper = ioHelper;
|
||||
_errors = new ModelsGenerationError(config, ioHelper);
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
_errors = new ModelsGenerationError(config, _hostingEnvironment);
|
||||
_ver = 1; // zero is for when we had no version
|
||||
_skipver = -1; // nothing to skip
|
||||
|
||||
RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted;
|
||||
|
||||
if (!hostingEnvironment.IsHosted) return;
|
||||
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper);
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
@@ -115,7 +109,7 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
infos.TryGetValue(contentTypeAlias, out var info);
|
||||
|
||||
// create model
|
||||
return info == null ? element : info.Ctor(element);
|
||||
return info == null ? element : info.Ctor(element, _publishedValueFallback);
|
||||
}
|
||||
|
||||
// this runs only once the factory is ready
|
||||
@@ -160,7 +154,6 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
#endregion
|
||||
|
||||
#region Compilation
|
||||
|
||||
// deadlock note
|
||||
//
|
||||
// when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager
|
||||
@@ -190,32 +183,33 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
//
|
||||
// well, that's what we are doing in this class' TheBuildManager property, using reflection.
|
||||
|
||||
private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
// private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// _locker.EnterReadLock();
|
||||
//
|
||||
// // just be safe - can happen if the first view is not an Umbraco view,
|
||||
// // or if something went wrong and we don't have an assembly at all
|
||||
// if (_modelsAssembly == null) return;
|
||||
//
|
||||
// if (_debugLevel > 0)
|
||||
// _logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
|
||||
// if (!(sender is RazorBuildProvider provider)) return;
|
||||
//
|
||||
// // add the assembly, and add a dependency to a text file that will change on each
|
||||
// // compilation as in some environments (could not figure which/why) the BuildManager
|
||||
// // would not re-compile the views when the models assembly is rebuilt.
|
||||
// provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly);
|
||||
// provider.AddVirtualPathDependency(ProjVirt);
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// if (_locker.IsReadLockHeld)
|
||||
// _locker.ExitReadLock();
|
||||
// }
|
||||
// }
|
||||
|
||||
// just be safe - can happen if the first view is not an Umbraco view,
|
||||
// or if something went wrong and we don't have an assembly at all
|
||||
if (_modelsAssembly == null) return;
|
||||
|
||||
if (_debugLevel > 0)
|
||||
_logger.Debug<PureLiveModelFactory>("RazorBuildProvider.CodeGenerationStarted");
|
||||
if (!(sender is RazorBuildProvider provider)) return;
|
||||
|
||||
// add the assembly, and add a dependency to a text file that will change on each
|
||||
// compilation as in some environments (could not figure which/why) the BuildManager
|
||||
// would not re-compile the views when the models assembly is rebuilt.
|
||||
provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly);
|
||||
provider.AddVirtualPathDependency(ProjVirt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsReadLockHeld)
|
||||
_locker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// tells the factory that it should build a new generation of models
|
||||
private void ResetModels()
|
||||
@@ -229,14 +223,12 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
_hasModels = false;
|
||||
_pendingRebuild = true;
|
||||
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper);
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
// clear stuff
|
||||
var modelsHashFile = Path.Combine(modelsDirectory, "models.hash");
|
||||
//var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs");
|
||||
//var projFile = Path.Combine(modelsDirectory, "all.generated.cs");
|
||||
var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path");
|
||||
|
||||
if (File.Exists(dllPathFile)) File.Delete(dllPathFile);
|
||||
@@ -249,17 +241,14 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
}
|
||||
}
|
||||
|
||||
// gets "the" build manager
|
||||
private BuildManager TheBuildManager
|
||||
// gets the RoslynCompiler
|
||||
private RoslynCompiler RoslynCompiler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_theBuildManager != null) return _theBuildManager;
|
||||
var prop = typeof(BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (prop == null)
|
||||
throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property.");
|
||||
_theBuildManager = (BuildManager)prop.GetValue(null);
|
||||
return _theBuildManager;
|
||||
if (_roslynCompiler != null) return _roslynCompiler;
|
||||
_roslynCompiler = new RoslynCompiler(System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies));
|
||||
return _roslynCompiler;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,12 +271,12 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
_locker.ExitReadLock();
|
||||
}
|
||||
|
||||
var buildManagerLocked = false;
|
||||
var roslynLocked = false;
|
||||
try
|
||||
{
|
||||
// always take the BuildManager lock *before* taking the _locker lock
|
||||
// to avoid possible deadlock situations (see notes above)
|
||||
Monitor.Enter(TheBuildManager, ref buildManagerLocked);
|
||||
Monitor.Enter(RoslynCompiler, ref roslynLocked);
|
||||
|
||||
_locker.EnterUpgradeableReadLock();
|
||||
|
||||
@@ -310,9 +299,6 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
// this is for U4-8043 which is an obvious issue but I cannot replicate
|
||||
//_modelsAssembly = _modelsAssembly ?? assembly;
|
||||
|
||||
// the one below is the normal one
|
||||
_modelsAssembly = assembly;
|
||||
|
||||
var types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
|
||||
_infos = RegisterModels(types);
|
||||
_errors.Clear();
|
||||
@@ -327,7 +313,6 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
}
|
||||
finally
|
||||
{
|
||||
_modelsAssembly = null;
|
||||
_infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
}
|
||||
}
|
||||
@@ -344,14 +329,36 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
_locker.ExitWriteLock();
|
||||
if (_locker.IsUpgradeableReadLockHeld)
|
||||
_locker.ExitUpgradeableReadLock();
|
||||
if (buildManagerLocked)
|
||||
Monitor.Exit(TheBuildManager);
|
||||
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))
|
||||
{
|
||||
_currentAssemblyLoadContext.Unload();
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
}
|
||||
|
||||
// We must create a new assembly load context
|
||||
// as long as theres a reference to the assembly load context we can't delete the assembly it loaded
|
||||
_currentAssemblyLoadContext = new UmbracoAssemblyLoadContext();
|
||||
|
||||
// Use filestream to load in the new assembly, otherwise it'll be locked
|
||||
// See https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/ for more info
|
||||
using (var fs = new FileStream(pathToAssembly, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
return _currentAssemblyLoadContext.LoadFromStream(fs);
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly GetModelsAssembly(bool forceRebuild)
|
||||
{
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper);
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
@@ -394,23 +401,16 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
// ensure that the .dll file does not have a corresponding .dll.delete file
|
||||
// as that would mean the the .dll file is going to be deleted and should not
|
||||
// be re-used - that should not happen in theory, but better be safe
|
||||
//
|
||||
// ensure that the .dll file is in the current codegen directory - when IIS
|
||||
// or Express does a full restart, it can switch to an entirely new codegen
|
||||
// directory, and then we end up referencing a dll which is *not* in that
|
||||
// directory, and BuildManager fails to instantiate views ("the view found
|
||||
// at ... was not created").
|
||||
//
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
var dllPath = File.ReadAllText(dllPathFile);
|
||||
var codegen = HttpRuntime.CodegenDir;
|
||||
|
||||
_logger.Debug<PureLiveModelFactory>($"Cached models dll at {dllPath}.");
|
||||
|
||||
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen))
|
||||
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete"))
|
||||
{
|
||||
assembly = Assembly.LoadFile(dllPath);
|
||||
assembly = ReloadAssembly(dllPath);
|
||||
|
||||
var attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
|
||||
if (attr != null && attr.PureLive && attr.SourceHash == currentHash)
|
||||
{
|
||||
@@ -430,8 +430,6 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
_logger.Debug<PureLiveModelFactory>("Cached models dll does not exist.");
|
||||
else if (File.Exists(dllPath + ".delete"))
|
||||
_logger.Debug<PureLiveModelFactory>("Cached models dll is marked for deletion.");
|
||||
else if (!dllPath.StartsWith(codegen))
|
||||
_logger.Debug<PureLiveModelFactory>("Cached models dll is in a different codegen directory.");
|
||||
else
|
||||
_logger.Debug<PureLiveModelFactory>("Cached models dll cannot be loaded (why?).");
|
||||
}
|
||||
@@ -446,16 +444,15 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
File.WriteAllText(projFile, text);
|
||||
}
|
||||
|
||||
// generate a marker file that will be a dependency
|
||||
// see note in RazorBuildProvider_CodeGenerationStarted
|
||||
// NO: using all.generated.cs as a dependency
|
||||
//File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver);
|
||||
|
||||
_ver++;
|
||||
try
|
||||
{
|
||||
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
|
||||
var assemblyPath = GetOutputAssemblyPath(currentHash);
|
||||
RoslynCompiler.CompileToFile(_hostingEnvironment.MapPathContentRoot(projFile), assemblyPath);
|
||||
assembly = ReloadAssembly(assemblyPath);
|
||||
File.WriteAllText(dllPathFile, assembly.Location);
|
||||
File.WriteAllText(modelsHashFile, currentHash);
|
||||
TryDeleteUnusedAssemblies(dllPathFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -492,9 +489,12 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
// compile and register
|
||||
try
|
||||
{
|
||||
assembly = BuildManager.GetCompiledAssembly(ProjVirt);
|
||||
File.WriteAllText(dllPathFile, assembly.Location);
|
||||
var assemblyPath = GetOutputAssemblyPath(currentHash);
|
||||
RoslynCompiler.CompileToFile(_hostingEnvironment.MapPathContentRoot(projFile), assemblyPath);
|
||||
assembly = ReloadAssembly(assemblyPath);
|
||||
File.WriteAllText(dllPathFile, assemblyPath);
|
||||
File.WriteAllText(modelsHashFile, currentHash);
|
||||
TryDeleteUnusedAssemblies(dllPathFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -506,6 +506,37 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private void TryDeleteUnusedAssemblies(string dllPathFile)
|
||||
{
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
catch(UnauthorizedAccessException)
|
||||
{
|
||||
// The file is in use, we'll try again next time...
|
||||
// This shouldn't happen anymore.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private string GetOutputAssemblyPath(string currentHash)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
|
||||
{
|
||||
_logger.Debug<PureLiveModelFactory>("Failed to compile.");
|
||||
@@ -525,7 +556,7 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
|
||||
private static Infos RegisterModels(IEnumerable<Type> types)
|
||||
{
|
||||
var ctorArgTypes = new[] { typeof(IPublishedElement) };
|
||||
var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
|
||||
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var map = new Dictionary<string, Type>();
|
||||
|
||||
@@ -537,7 +568,7 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
foreach (var ctor in type.GetConstructors())
|
||||
{
|
||||
var parms = ctor.GetParameters();
|
||||
if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType))
|
||||
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.");
|
||||
@@ -562,9 +593,10 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
|
||||
var gen = meth.GetILGenerator();
|
||||
gen.Emit(OpCodes.Ldarg_0);
|
||||
gen.Emit(OpCodes.Ldarg_1);
|
||||
gen.Emit(OpCodes.Newobj, constructor);
|
||||
gen.Emit(OpCodes.Ret);
|
||||
var func = (Func<IPublishedElement, IPublishedElement>)meth.CreateDelegate(typeof(Func<IPublishedElement, IPublishedElement>));
|
||||
var func = (Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>)meth.CreateDelegate(typeof(Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>));
|
||||
|
||||
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type };
|
||||
map[typeName] = type;
|
||||
@@ -575,7 +607,7 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
|
||||
private string GenerateModelsCode(IList<TypeModel> typeModels)
|
||||
{
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper);
|
||||
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
|
||||
if (!Directory.Exists(modelsDirectory))
|
||||
Directory.CreateDirectory(modelsDirectory);
|
||||
|
||||
@@ -653,7 +685,7 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
internal class ModelInfo
|
||||
{
|
||||
public Type ParameterType { get; set; }
|
||||
public Func<IPublishedElement, IPublishedElement> Ctor { get; set; }
|
||||
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
|
||||
public Type ModelType { get; set; }
|
||||
public Func<IList> ListCtor { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user