using System;
using System.Data;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Infrastructure.Persistence;
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
using Umbraco.Extensions;
#if DEBUG_SCOPES
using System.Collections.Generic;
using System.Linq;
using System.Text;
#endif
namespace Umbraco.Cms.Core.Scoping
{
///
/// Implements .
///
internal class ScopeProvider : IScopeProvider, IScopeAccessor
{
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IRequestCache _requestCache;
private readonly FileSystems _fileSystems;
private readonly CoreDebugSettings _coreDebugSettings;
private readonly IMediaFileSystem _mediaFileSystem;
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger logger, ILoggerFactory loggerFactory, IRequestCache requestCache)
{
DatabaseFactory = databaseFactory;
_fileSystems = fileSystems;
_coreDebugSettings = coreDebugSettings.Value;
_mediaFileSystem = mediaFileSystem;
_logger = logger;
_loggerFactory = loggerFactory;
_requestCache = requestCache;
// take control of the FileSystems
_fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems;
_scopeReference = new ScopeReference(this);
}
public IUmbracoDatabaseFactory DatabaseFactory { get; }
public ISqlContext SqlContext => DatabaseFactory.SqlContext;
#region Context
private static T GetCallContextObject(string key)
where T : class, IInstanceIdentifiable
{
T obj = CallContext.GetData(key);
if (obj == default(T))
{
return null;
}
return obj;
}
private static void SetCallContextObject(string key, T value, ILogger logger)
where T : class, IInstanceIdentifiable
{
#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
IScope ambientScope = CallContext.GetData(ScopeItemKey);
if (ambientScope != null)
{
RegisterContext(ambientScope, logger, null);
}
// then register the new value
if (value is IScope scope)
{
RegisterContext(scope, logger, "call");
}
}
#endif
if (value == null)
{
T obj = CallContext.GetData(key);
CallContext.SetData(key, default); // aka remove
if (obj == null)
{
return;
}
}
else
{
#if DEBUG_SCOPES
logger.LogDebug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
#endif
CallContext.SetData(key, value);
}
}
private T GetHttpContextObject(string key, bool required = true)
where T : class
{
if (!_requestCache.IsAvailable && required)
{
throw new Exception("Request cache is unavailable.");
}
return (T)_requestCache.Get(key);
}
private bool SetHttpContextObject(string key, object value, bool required = true)
{
if (!_requestCache.IsAvailable)
{
if (required)
{
throw new Exception("Request cache is unavailable.");
}
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)_requestCache.Get(ScopeItemKey);
if (ambientScope != null)
{
RegisterContext(ambientScope, _logger, null);
}
// then register the new value
if (value is IScope scope)
{
RegisterContext(scope, _logger, "http");
}
}
#endif
if (value == null)
{
_requestCache.Remove(key);
}
else
{
_requestCache.Set(key, value);
}
return true;
}
#endregion
#region Ambient Context
internal static readonly string ContextItemKey = $"{typeof(ScopeProvider).FullName}";
///
/// Get or set the Ambient (Current) for the current execution context.
///
///
/// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal)
///
public IScopeContext AmbientContext
{
get
{
// try http context, fallback onto call context
IScopeContext value = GetHttpContextObject(ContextItemKey, false);
return value ?? GetCallContextObject(ContextItemKey);
}
set
{
// clear both
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null, _logger);
if (value == null)
{
return;
}
// set http/call context
if (SetHttpContextObject(ContextItemKey, value, false) == false)
{
SetCallContextObject(ContextItemKey, value, _logger);
}
}
}
#endregion
#region Ambient Scope
internal static readonly string ScopeItemKey = typeof(Scope).FullName;
internal static readonly string ScopeRefItemKey = typeof(ScopeReference).FullName;
// only 1 instance which can be disposed and disposed again
private readonly ScopeReference _scopeReference;
IScope IScopeAccessor.AmbientScope => AmbientScope;
///
/// Get or set the Ambient (Current) for the current execution context.
///
///
/// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal)
///
public Scope AmbientScope
{
// try http context, fallback onto call context
// we are casting here because we know its a concrete type
get => (Scope)GetHttpContextObject(ScopeItemKey, false)
?? (Scope)GetCallContextObject(ScopeItemKey);
set
{
// clear both
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null, _logger);
if (value == null)
{
return;
}
// set http/call context
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
{
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
}
else
{
SetCallContextObject(ScopeItemKey, value, _logger);
}
}
}
#endregion
///
/// Set the Ambient (Current) and for the current execution context.
///
///
/// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal)
///
public void SetAmbient(Scope scope, IScopeContext context = null)
{
// clear all
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null, _logger);
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null, _logger);
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, _logger);
SetCallContextObject(ContextItemKey, context, _logger);
}
}
///
public IScope CreateDetachedScope(
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null)
{
return new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
}
///
public void AttachScope(IScope other, bool callContext = false)
{
// IScopeProvider.AttachScope works with an IScope
// but here we can only deal with our own Scope class
if (!(other is Scope otherScope))
throw new ArgumentException("Not a Scope instance.");
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);
}
///
public IScope DetachScope()
{
Scope ambientScope = AmbientScope;
if (ambientScope == null)
{
throw new InvalidOperationException("There is no ambient scope.");
}
if (ambientScope.Detachable == false)
{
throw new InvalidOperationException("Ambient scope is not detachable.");
}
SetAmbient(ambientScope.OrigScope, ambientScope.OrigContext);
ambientScope.OrigScope = null;
ambientScope.OrigContext = null;
ambientScope.Attached = false;
return ambientScope;
}
///
public IScope CreateScope(
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
{
Scope ambientScope = AmbientScope;
if (ambientScope == null)
{
IScopeContext ambientContext = AmbientContext;
ScopeContext newContext = ambientContext == null ? new ScopeContext() : null;
var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
// assign only if scope creation did not throw!
SetAmbient(scope, newContext ?? ambientContext);
return scope;
}
var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
SetAmbient(nested, AmbientContext);
return nested;
}
public void Reset()
{
var scope = AmbientScope;
scope?.Reset();
_scopeReference.Dispose();
}
///
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("CallContext: Scheduled Publishing");
// else if (trace.IndexOf("TouchServerTask") > 0)
// LogHelper.Debug("CallContext: Server Registration");
// else if (trace.IndexOf("LogScrubber") > 0)
// LogHelper.Debug("CallContext: Log Scrubber");
// else
// LogHelper.Debug("CallContext: " + Environment.StackTrace);
//}
// all scope instances that are currently being tracked
private static readonly object s_staticScopeInfosLock = new object();
private static readonly Dictionary s_staticScopeInfos = new Dictionary();
public IEnumerable ScopeInfos
{
get
{
lock (s_staticScopeInfosLock)
{
return s_staticScopeInfos.Values.ToArray(); // capture in an array
}
}
}
public ScopeInfo GetScopeInfo(IScope scope)
{
lock (s_staticScopeInfosLock)
{
return s_staticScopeInfos.TryGetValue(scope, out ScopeInfo scopeInfo) ? scopeInfo : null;
}
}
// register a scope and capture its ctor stacktrace
public void RegisterScope(IScope scope)
{
lock (s_staticScopeInfosLock)
{
if (s_staticScopeInfos.ContainsKey(scope))
{
throw new Exception("oops: already registered.");
}
_logger.LogDebug("Register " + scope.InstanceId.ToString("N").Substring(0, 8));
s_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, ILogger logger, string context)
{
lock (s_staticScopeInfosLock)
{
if (s_staticScopeInfos.TryGetValue(scope, out ScopeInfo info) == false)
{
info = null;
}
if (info == null)
{
if (context == null)
{
return;
}
throw new Exception("oops: unregistered scope.");
}
var sb = new StringBuilder();
IScope s = scope;
while (s != null)
{
if (sb.Length > 0)
{
sb.Append(" < ");
}
sb.Append(s.InstanceId.ToString("N").Substring(0, 8));
var ss = s as Scope;
s = ss?.ParentScope;
}
logger?.LogTrace("Register " + (context ?? "null") + " context " + sb);
if (context == null)
{
info.NullStack = Environment.StackTrace;
}
logger?.LogTrace("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 (s_staticScopeInfosLock)
{
if (s_staticScopeInfos.ContainsKey(scope))
{
// enable this by default
//Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8));
s_staticScopeInfos.Remove(scope);
_logger.LogDebug("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; } // the scope itself
// the scope's parent identifier
public Guid Parent => ((Scope)Scope).ParentScope == null ? Guid.Empty : ((Scope)Scope).ParentScope.InstanceId;
public DateTime Created { get; } // 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; } // 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
public override string ToString() => new StringBuilder()
.AppendLine("ScopeInfo:")
.Append("Instance Id: ")
.AppendLine(Scope.InstanceId.ToString())
.Append("Parent Id: ")
.AppendLine(Parent.ToString())
.Append("Created Thread Id: ")
.AppendLine(Scope.CreatedThreadId.ToInvariantString())
.Append("Created At: ")
.AppendLine(Created.ToString("O"))
.Append("Disposed: ")
.AppendLine(Disposed.ToString())
.Append("CTOR stack: ")
.AppendLine(CtorStack)
.ToString();
}
#endif
}