Introduce SafeCallContext to fix breaking test

(wondering if using logical call context a breaking change)
This commit is contained in:
Stephan
2016-09-12 18:36:08 +02:00
parent d84cfe001f
commit 597b9bbfcb
10 changed files with 217 additions and 53 deletions

View File

@@ -29,20 +29,24 @@ namespace Umbraco.Core.Packaging
/// </remarks>
public static IEnumerable<string> ScanAssembliesForTypeReference<T>(IEnumerable<byte[]> assemblys, out string[] errorReport)
{
var appDomain = GetTempAppDomain();
var type = typeof(PackageBinaryInspector);
try
// beware! when toying with domains, use a safe call context!
using (new SafeCallContext())
{
var value = (PackageBinaryInspector)appDomain.CreateInstanceAndUnwrap(
type.Assembly.FullName,
type.FullName);
// do NOT turn PerformScan into static (even if ReSharper says so)!
var result = value.PerformScan<T>(assemblys.ToArray(), out errorReport);
return result;
}
finally
{
AppDomain.Unload(appDomain);
var appDomain = GetTempAppDomain();
var type = typeof(PackageBinaryInspector);
try
{
var value = (PackageBinaryInspector) appDomain.CreateInstanceAndUnwrap(
type.Assembly.FullName,
type.FullName);
// do NOT turn PerformScan into static (even if ReSharper says so)!
var result = value.PerformScan<T>(assemblys.ToArray(), out errorReport);
return result;
}
finally
{
AppDomain.Unload(appDomain);
}
}
}
@@ -281,12 +285,8 @@ namespace Umbraco.Core.Packaging
PrivateBinPathProbe = AppDomain.CurrentDomain.SetupInformation.PrivateBinPathProbe
};
//create new domain with full trust
return AppDomain.CreateDomain(
appName,
AppDomain.CurrentDomain.Evidence,
domainSetup,
new PermissionSet(PermissionState.Unrestricted));
// create new domain with full trust
return AppDomain.CreateDomain(appName, AppDomain.CurrentDomain.Evidence, domainSetup, new PermissionSet(PermissionState.Unrestricted));
}
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Runtime.Remoting.Messaging;
using System.Web;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
namespace Umbraco.Core.Persistence
@@ -46,9 +45,12 @@ namespace Umbraco.Core.Persistence
/// <param name="logger"></param>
public DefaultDatabaseFactory(string connectionStringName, ILogger logger)
{
if (logger == null) throw new ArgumentNullException("logger");
if (logger == null) throw new ArgumentNullException("logger");
Mandate.ParameterNotNullOrEmpty(connectionStringName, "connectionStringName");
_connectionStringName = connectionStringName;
//if (NonContextValue != null) throw new Exception("NonContextValue is not null.");
_connectionStringName = connectionStringName;
_logger = logger;
}
@@ -60,10 +62,13 @@ namespace Umbraco.Core.Persistence
/// <param name="logger"></param>
public DefaultDatabaseFactory(string connectionString, string providerName, ILogger logger)
{
if (logger == null) throw new ArgumentNullException("logger");
if (logger == null) throw new ArgumentNullException("logger");
Mandate.ParameterNotNullOrEmpty(connectionString, "connectionString");
Mandate.ParameterNotNullOrEmpty(providerName, "providerName");
ConnectionString = connectionString;
//if (NonContextValue != null) throw new Exception("NonContextValue is not null.");
ConnectionString = connectionString;
ProviderName = providerName;
_logger = logger;
}
@@ -120,29 +125,14 @@ namespace Umbraco.Core.Persistence
else
{
var db = (UmbracoDatabase) HttpContext.Current.Items[typeof(DefaultDatabaseFactory)];
if (db != null)
{
db.Dispose();
HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory));
}
if (db != null) db.Dispose();
HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory));
}
}
protected override void DisposeResources()
{
if (HttpContext.Current == null)
{
var value = NonContextValue;
if (value != null) value.Dispose();
NonContextValue = null;
}
else
{
if (HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)))
{
((UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]).Dispose();
}
}
ReleaseDatabase();
}
internal void ResetForTests()
@@ -151,5 +141,56 @@ namespace Umbraco.Core.Persistence
if (value != null) value.Dispose();
NonContextValue = null;
}
#region SafeCallContext
// see notes in SafeCallContext - need to do this since we are using
// the logical call context...
static DefaultDatabaseFactory()
{
SafeCallContext.Register(DetachDatabase, AttachDatabase);
}
// detaches the current database
// ie returns the database and remove it from whatever is "context"
private static UmbracoDatabase DetachDatabase()
{
if (HttpContext.Current == null)
{
var db = NonContextValue;
NonContextValue = null;
return db;
}
else
{
var db = (UmbracoDatabase) HttpContext.Current.Items[typeof(DefaultDatabaseFactory)];
HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory));
return db;
}
}
// attach a current database
// ie assign it to whatever is "context"
// throws if there already is a database
private static void AttachDatabase(object o)
{
if (o == null) return;
var database = o as UmbracoDatabase;
if (database == null) throw new ArgumentException("Not an UmbracoDatabase.", "o");
if (HttpContext.Current == null)
{
if (NonContextValue != null) throw new InvalidOperationException();
NonContextValue = database;
}
else
{
if (HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] != null) throw new InvalidOperationException();
HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] = database;
}
}
#endregion
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Text;
using StackExchange.Profiling;
using Umbraco.Core.Logging;

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using Umbraco.Core.Persistence;
namespace Umbraco.Core
{
internal class SafeCallContext : IDisposable
{
private static readonly List<Func<object>> EnterFuncs = new List<Func<object>>();
private static readonly List<Action<object>> ExitActions = new List<Action<object>>();
private static int _count;
private readonly List<object> _objects;
private bool _disposed;
public static void Register(Func<object> enterFunc, Action<object> exitAction)
{
if (enterFunc == null) throw new ArgumentNullException("enterFunc");
if (exitAction == null) throw new ArgumentNullException("exitAction");
lock (EnterFuncs)
{
if (_count > 0) throw new Exception("Busy.");
EnterFuncs.Add(enterFunc);
ExitActions.Add(exitAction);
}
}
public static void ClearCallContext()
{
lock (EnterFuncs)
{
var ignore = EnterFuncs.Select(x => x()).ToList();
}
}
// tried to make the UmbracoDatabase serializable but then it leaks to weird places
// in ReSharper and so on, where Umbraco.Core is not available. Tried to serialize
// as an object instead but then it comes *back* deserialized into the original context
// as an object and of course it breaks everything. Cannot prevent this from flowing,
// and ExecutionContext.SuppressFlow() works for threads but not domains. and we'll
// have the same issue with anything that toys with logical call context... so this
// class let them register & deal with the situation (removing themselves)
public SafeCallContext()
{
Interlocked.Increment(ref _count);
lock (EnterFuncs)
{
_count++;
_objects = EnterFuncs.Select(x => x()).ToList();
}
}
public void Dispose()
{
if (_disposed) throw new ObjectDisposedException("this");
_disposed = true;
lock (EnterFuncs)
{
for (var i = 0; i < ExitActions.Count; i++)
ExitActions[i](_objects[i]);
_count--;
}
}
}
}

View File

@@ -420,6 +420,7 @@
<Compile Include="Models\UmbracoDomain.cs" />
<Compile Include="Models\DoNotCloneAttribute.cs" />
<Compile Include="Models\IDomain.cs" />
<Compile Include="SafeCallContext.cs" />
<Compile Include="Persistence\DatabaseNodeLockExtensions.cs" />
<Compile Include="Persistence\Factories\ExternalLoginFactory.cs" />
<Compile Include="Persistence\Factories\MigrationEntryFactory.cs" />

View File

@@ -0,0 +1,44 @@
using System.Runtime.Remoting.Messaging;
using NUnit.Framework;
namespace Umbraco.Tests
{
[TestFixture]
public class CallContextTests
{
[SetUp]
public void Setup()
{
ClearCallContext();
}
[TearDown]
public void TearDown()
{
ClearCallContext();
}
private static void ClearCallContext()
{
// logical call context leaks between tests
// cleanup things before/after we've run else one of
// the two tests will fail
CallContext.FreeNamedDataSlot("test1");
CallContext.FreeNamedDataSlot("test2");
}
[Test]
public void Test1()
{
CallContext.LogicalSetData("test1", "test1");
Assert.IsNull(CallContext.LogicalGetData("test2"));
}
[Test]
public void Test2()
{
CallContext.LogicalSetData("test2", "test2");
Assert.IsNull(CallContext.LogicalGetData("test1"));
}
}
}

View File

@@ -23,6 +23,9 @@ namespace Umbraco.Tests.Persistence
[SetUp]
public void Setup()
{
// bah
SafeCallContext.ClearCallContext();
_dbContext = new DatabaseContext(
new DefaultDatabaseFactory(Core.Configuration.GlobalSettings.UmbracoConnectionName, Mock.Of<ILogger>()),
Mock.Of<ILogger>(), new SqlCeSyntaxProvider(), Constants.DatabaseProviders.SqlCe);
@@ -98,7 +101,7 @@ namespace Umbraco.Tests.Persistence
var schemaHelper = new DatabaseSchemaHelper(_dbContext.Database, Mock.Of<ILogger>(), new SqlCeSyntaxProvider());
var appCtx = new ApplicationContext(
new DatabaseContext(Mock.Of<IDatabaseFactory>(), Mock.Of<ILogger>(), Mock.Of<ISqlSyntaxProvider>(), "test"),
_dbContext,
new ServiceContext(migrationEntryService: Mock.Of<IMigrationEntryService>()),
CacheHelper.CreateDisabledCacheHelper(),
new ProfilingLogger(Mock.Of<ILogger>(), Mock.Of<IProfiler>()));

View File

@@ -61,6 +61,11 @@ namespace Umbraco.Tests.TestHelpers
var path = TestHelper.CurrentAssemblyDirectory;
AppDomain.CurrentDomain.SetData("DataDirectory", path);
// fixme - in theory we should not need this
// it's done in base.Initialize() and no idea why we do things
// in this order here... not going to change it now though...
SafeCallContext.ClearCallContext();
_dbFactory = new DefaultDatabaseFactory(
GetDbConnectionString(),
GetDbProviderName(),

View File

@@ -1,6 +1,5 @@
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Configuration.UmbracoSettings;
namespace Umbraco.Tests.TestHelpers
{
@@ -10,21 +9,25 @@ namespace Umbraco.Tests.TestHelpers
[TestFixture]
public abstract class BaseUmbracoConfigurationTest
{
[SetUp]
public virtual void Initialize()
{
SettingsForTests.Reset();
Reset();
}
[TearDown]
public virtual void TearDown()
{
//reset settings
SettingsForTests.Reset();
Reset();
}
private static void Reset()
{
// reset settings
SettingsForTests.Reset();
// clear the logical call context
SafeCallContext.ClearCallContext();
}
}
}

View File

@@ -175,6 +175,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="CallContextTests.cs" />
<Compile Include="Migrations\MigrationIssuesTests.cs" />
<Compile Include="Persistence\Migrations\MigrationStartupHandlerTests.cs" />
<Compile Include="Persistence\PetaPocoExpressionsTests.cs" />