Files
Umbraco-CMS/src/Umbraco.Core/Scoping/ScopeProvider.cs
2017-06-02 14:00:09 +02:00

562 lines
22 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Runtime.Remoting.Messaging;
using System.Web;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
#if DEBUG_SCOPES
using System.Linq;
#endif
namespace Umbraco.Core.Scoping
{
/// <summary>
/// Implements <see cref="IScopeProvider"/>.
/// </summary>
internal class ScopeProvider : IScopeProvider
{
private readonly ILogger _logger;
private readonly FileSystems _fileSystems;
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, ILogger logger)
{
DatabaseFactory = databaseFactory;
_fileSystems = fileSystems;
_logger = logger;
// take control of the FileSystems
_fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems;
_scopeReference = new ScopeReference(this);
}
static ScopeProvider()
{
SafeCallContext.Register(
() =>
{
var scope = GetCallContextObject<Scope>(ScopeItemKey);
var context = GetCallContextObject<ScopeContext>(ContextItemKey);
SetCallContextObject(ScopeItemKey, null);
SetCallContextObject(ContextItemKey, null);
return Tuple.Create(scope, context);
},
o =>
{
// cannot re-attached over leaked scope/context
if (GetCallContextObject<Scope>(ScopeItemKey) != null)
throw new Exception("Found leaked scope when restoring call context.");
if (GetCallContextObject<ScopeContext>(ContextItemKey) != null)
throw new Exception("Found leaked context when restoring call context.");
var t = (Tuple<Scope, ScopeContext>) o;
SetCallContextObject(ScopeItemKey, t.Item1);
SetCallContextObject(ContextItemKey, t.Item2);
});
}
public IUmbracoDatabaseFactory DatabaseFactory { get; }
#region Context
// objects that go into the logical call context better be serializable else they'll eventually
// cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running
// tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx),
// but we don't want to make all of our objects serializable since they are *not* meant to be
// used in cross-AppDomain scenario anyways.
// in addition, whatever goes into the logical call context is serialized back and forth any
// time cross-AppDomain code executes, so if we put an "object" there, we'll can *another*
// "object" instance - and so we cannot use a random object as a key.
// so what we do is: we register a guid in the call context, and we keep a table mapping those
// guids to the actual objects. the guid serializes back and forth without causing any issue,
// and we can retrieve the actual objects from the table.
// only issue: how are we supposed to clear the table? we can't, really. objects should take
// care of de-registering themselves from context.
// everything we use does, except the NoScope scope, which just stays there
//
// during tests, NoScope can to into call context... nothing much we can do about it
private static readonly object StaticCallContextObjectsLock = new object();
private static readonly Dictionary<Guid, object> StaticCallContextObjects
= new Dictionary<Guid, object>();
#if DEBUG_SCOPES
public Dictionary<Guid, object> CallContextObjects
{
get
{
lock (StaticCallContextObjectsLock)
{
// capture in a dictionary
return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value);
}
}
}
#endif
private static T GetCallContextObject<T>(string key)
where T : class
{
var objectKey = CallContext.LogicalGetData(key).AsGuid();
if (objectKey == Guid.Empty) return null;
lock (StaticCallContextObjectsLock)
{
if (StaticCallContextObjects.TryGetValue(objectKey, out object callContextObject))
{
#if DEBUG_SCOPES
Logging.LogHelper.Debug<ScopeProvider>("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8));
//Logging.LogHelper.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
return (T)callContextObject;
}
// hard to inject into a static method :(
Current.Logger.Warn<ScopeProvider>("Missed " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8));
#if DEBUG_SCOPES
//Logging.LogHelper.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
return null;
}
}
private static void SetCallContextObject(string key, IInstanceIdentifiable value)
{
#if DEBUG_SCOPES
// manage the 'context' that contains the scope (null, "http" or "call")
// only for scopes of course!
if (key == ScopeItemKey)
{
// first, null-register the existing value
var ambientKey = CallContext.LogicalGetData(ScopeItemKey).AsGuid();
object o = null;
lock (StaticCallContextObjectsLock)
{
if (ambientKey != default(Guid))
StaticCallContextObjects.TryGetValue(ambientKey, out o);
}
var ambientScope = o as IScope;
if (ambientScope != null) RegisterContext(ambientScope, null);
// then register the new value
var scope = value as IScope;
if (scope != null) RegisterContext(scope, "call");
}
#endif
if (value == null)
{
var objectKey = CallContext.LogicalGetData(key).AsGuid();
CallContext.FreeNamedDataSlot(key);
if (objectKey == default (Guid)) return;
lock (StaticCallContextObjectsLock)
{
#if DEBUG_SCOPES
Logging.LogHelper.Debug<ScopeProvider>("Remove Object " + objectKey.ToString("N").Substring(0, 8));
//Logging.LogHelper.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
StaticCallContextObjects.Remove(objectKey);
}
}
else
{
// note - we are *not* detecting an already-existing value
// because our code in this class *always* sets to null before
// setting to a real value
var objectKey = value.InstanceId;
lock (StaticCallContextObjectsLock)
{
#if DEBUG_SCOPES
Logging.LogHelper.Debug<ScopeProvider>("AddObject " + objectKey.ToString("N").Substring(0, 8));
//Logging.LogHelper.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
StaticCallContextObjects.Add(objectKey, value);
}
CallContext.LogicalSetData(key, objectKey);
}
}
// this is for tests exclusively until we have a proper accessor in v8
internal static Func<IDictionary> HttpContextItemsGetter { get; set; }
private static IDictionary HttpContextItems => HttpContextItemsGetter == null
? HttpContext.Current?.Items
: HttpContextItemsGetter();
public static T GetHttpContextObject<T>(string key, bool required = true)
where T : class
{
var httpContextItems = HttpContextItems;
if (httpContextItems != null)
return (T)httpContextItems[key];
if (required)
throw new Exception("HttpContext.Current is null.");
return null;
}
private static bool SetHttpContextObject(string key, object value, bool required = true)
{
var httpContextItems = HttpContextItems;
if (httpContextItems == null)
{
if (required)
throw new Exception("HttpContext.Current is null.");
return false;
}
#if DEBUG_SCOPES
// manage the 'context' that contains the scope (null, "http" or "call")
// only for scopes of course!
if (key == ScopeItemKey)
{
// first, null-register the existing value
var ambientScope = (IScope)httpContextItems[ScopeItemKey];
if (ambientScope != null) RegisterContext(ambientScope, null);
// then register the new value
var scope = value as IScope;
if (scope != null) RegisterContext(scope, "http");
}
#endif
if (value == null)
httpContextItems.Remove(key);
else
httpContextItems[key] = value;
return true;
}
#endregion
#region Ambient Context
internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext";
public ScopeContext AmbientContext
{
get
{
// try http context, fallback onto call context
var value = GetHttpContextObject<ScopeContext>(ContextItemKey, false);
return value ?? GetCallContextObject<ScopeContext>(ContextItemKey);
}
set
{
// clear both
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null);
if (value == null) return;
// set http/call context
if (SetHttpContextObject(ContextItemKey, value, false) == false)
SetCallContextObject(ContextItemKey, value);
}
}
#endregion
#region Ambient Scope
internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope";
internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference";
// only 1 instance which can be disposed and disposed again
private readonly ScopeReference _scopeReference;
public Scope AmbientScope
{
get
{
// try http context, fallback onto call context
var value = GetHttpContextObject<Scope>(ScopeItemKey, false);
return value ?? GetCallContextObject<Scope>(ScopeItemKey);
}
set
{
// clear both
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null);
if (value == null) return;
// set http/call context
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
else
SetCallContextObject(ScopeItemKey, value);
}
}
#endregion
public void SetAmbient(Scope scope, ScopeContext context = null)
{
// clear all
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null);
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null);
if (scope == null)
{
if (context != null)
throw new ArgumentException("Must be null if scope is null.", nameof(context));
return;
}
if (scope.CallContext == false && SetHttpContextObject(ScopeItemKey, scope, false))
{
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
SetHttpContextObject(ContextItemKey, context);
}
else
{
SetCallContextObject(ScopeItemKey, scope);
SetCallContextObject(ContextItemKey, context);
}
}
/// <inheritdoc />
public IScope CreateDetachedScope(
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null)
{
return new Scope(this, _logger, _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
}
/// <inheritdoc />
public void AttachScope(IScope other, bool callContext = false)
{
var otherScope = other as Scope;
if (otherScope == null)
throw new ArgumentException("Not a Scope instance."); // fixme - why? how?
if (otherScope.Detachable == false)
throw new ArgumentException("Not a detachable scope.");
if (otherScope.Attached)
throw new InvalidOperationException("Already attached.");
otherScope.Attached = true;
otherScope.OrigScope = AmbientScope;
otherScope.OrigContext = AmbientContext;
otherScope.CallContext = callContext;
SetAmbient(otherScope, otherScope.Context);
}
/// <inheritdoc />
public IScope DetachScope()
{
var ambient = AmbientScope;
if (ambient == null)
throw new InvalidOperationException("There is no ambient scope.");
var scope = ambient as Scope;
if (scope == null)
throw new Exception("Ambient scope is not a Scope instance."); // fixme - why? how?
if (scope.Detachable == false)
throw new InvalidOperationException("Ambient scope is not detachable.");
SetAmbient(scope.OrigScope, scope.OrigContext);
scope.OrigScope = null;
scope.OrigContext = null;
scope.Attached = false;
return scope;
}
/// <inheritdoc />
public IScope CreateScope(
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false)
{
var ambient = AmbientScope;
if (ambient == null)
{
var ambientContext = AmbientContext;
var newContext = ambientContext == null ? new ScopeContext() : null;
var scope = new Scope(this, _logger, _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext);
// assign only if scope creation did not throw!
SetAmbient(scope, newContext ?? ambientContext);
return scope;
}
var ambientScope = ambient as Scope;
if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); // fixme - why? how?
var nested = new Scope(this, _logger, _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext);
SetAmbient(nested, AmbientContext);
return nested;
}
/// <inheritdoc />
public void Reset()
{
var scope = AmbientScope as Scope;
scope?.Reset();
_scopeReference.Dispose();
}
/// <inheritdoc />
public IScopeContext Context => AmbientContext;
#if DEBUG_SCOPES
// this code needs TLC
//
// the idea here is to keep in a list all the scopes that have been created, and to remove them
// when they are disposed, so we can track leaks, ie scopes that would not be properly taken
// care of by our code
//
// note: the code could probably be optimized... but this is NOT supposed to go into any real
// live build, either production or debug - it's just a debugging tool for the time being
// helps identifying when non-httpContext scopes are created by logging the stack trace
//private void LogCallContextStack()
//{
// var trace = Environment.StackTrace;
// if (trace.IndexOf("ScheduledPublishing") > 0)
// LogHelper.Debug<ScopeProvider>("CallContext: Scheduled Publishing");
// else if (trace.IndexOf("TouchServerTask") > 0)
// LogHelper.Debug<ScopeProvider>("CallContext: Server Registration");
// else if (trace.IndexOf("LogScrubber") > 0)
// LogHelper.Debug<ScopeProvider>("CallContext: Log Scrubber");
// else
// LogHelper.Debug<ScopeProvider>("CallContext: " + Environment.StackTrace);
//}
// all scope instances that are currently beeing tracked
private static readonly object StaticScopeInfosLock = new object();
private static readonly Dictionary<IScope, ScopeInfo> StaticScopeInfos = new Dictionary<IScope, ScopeInfo>();
public IEnumerable<ScopeInfo> ScopeInfos
{
get
{
lock (StaticScopeInfosLock)
{
return StaticScopeInfos.Values.ToArray(); // capture in an array
}
}
}
public ScopeInfo GetScopeInfo(IScope scope)
{
lock (StaticScopeInfosLock)
{
ScopeInfo scopeInfo;
return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null;
}
}
//private static void Log(string message, UmbracoDatabase database)
//{
// LogHelper.Debug<ScopeProvider>(message + " (" + (database == null ? "" : database.InstanceSid) + ").");
//}
// register a scope and capture its ctor stacktrace
public void RegisterScope(IScope scope)
{
lock (StaticScopeInfosLock)
{
if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered.");
Logging.LogHelper.Debug<ScopeProvider>("Register " + scope.InstanceId.ToString("N").Substring(0, 8));
StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace);
}
}
// register that a scope is in a 'context'
// 'context' that contains the scope (null, "http" or "call")
public static void RegisterContext(IScope scope, string context)
{
lock (StaticScopeInfosLock)
{
ScopeInfo info;
if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null;
if (info == null)
{
if (context == null) return;
throw new Exception("oops: unregistered scope.");
}
var sb = new StringBuilder();
var s = scope;
while (s != null)
{
if (sb.Length > 0) sb.Append(" < ");
sb.Append(s.InstanceId.ToString("N").Substring(0, 8));
var ss = s as IScopeInternal;
s = ss == null ? null : ss.ParentScope;
}
Logging.LogHelper.Debug<ScopeProvider>("Register " + (context ?? "null") + " context " + sb);
if (context == null) info.NullStack = Environment.StackTrace;
//Logging.LogHelper.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 16));
info.Context = context;
}
}
private static string Head(string s, int count)
{
var pos = 0;
var i = 0;
while (i < count && pos >= 0)
{
pos = s.IndexOf("\r\n", pos + 1, StringComparison.OrdinalIgnoreCase);
i++;
}
if (pos < 0) return s;
return s.Substring(0, pos);
}
public void Disposed(IScope scope)
{
lock (StaticScopeInfosLock)
{
if (StaticScopeInfos.ContainsKey(scope))
{
// enable this by default
//Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8));
StaticScopeInfos.Remove(scope);
Logging.LogHelper.Debug<ScopeProvider>("Remove " + scope.InstanceId.ToString("N").Substring(0, 8));
// instead, enable this to keep *all* scopes
// beware, there can be a lot of scopes!
//info.Disposed = true;
//info.DisposedStack = Environment.StackTrace;
}
}
}
#endif
}
#if DEBUG_SCOPES
public class ScopeInfo
{
public ScopeInfo(IScope scope, string ctorStack)
{
Scope = scope;
Created = DateTime.Now;
CtorStack = ctorStack;
}
public IScope Scope { get; private set; } // the scope itself
// the scope's parent identifier
public Guid Parent { get { return (Scope is NoScope || ((Scope) Scope).ParentScope == null) ? Guid.Empty : ((Scope) Scope).ParentScope.InstanceId; } }
public DateTime Created { get; private set; } // the date time the scope was created
public bool Disposed { get; set; } // whether the scope has been disposed already
public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc")
public string CtorStack { get; private set; } // the stacktrace of the scope ctor
public string DisposedStack { get; set; } // the stacktrace when disposed
public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null
}
#endif
}