Files
Umbraco-CMS/src/Umbraco.Core/Runtime/MainDom.cs
2020-09-16 13:46:45 +02:00

253 lines
9.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Hosting;
namespace Umbraco.Core.Runtime
{
/// <summary>
/// Provides the full implementation of <see cref="IMainDom"/>.
/// </summary>
/// <remarks>
/// <para>When an AppDomain starts, it tries to acquire the main domain status.</para>
/// <para>When an AppDomain stops (eg the application is restarting) it should release the main domain status.</para>
/// </remarks>
public class MainDom : IMainDom, IRegisteredObject, IDisposable
{
#region Vars
private readonly ILogger<MainDom> _logger;
private IApplicationShutdownRegistry _hostingEnvironment;
private readonly IMainDomLock _mainDomLock;
// our own lock for local consistency
private object _locko = new object();
private bool _isInitialized;
// indicates whether...
private bool _isMainDom; // we are the main domain
private volatile bool _signaled; // we have been signaled
// actions to run before releasing the main domain
private readonly List<KeyValuePair<int, Action>> _callbacks = new List<KeyValuePair<int, Action>>();
private const int LockTimeoutMilliseconds = 40000; // 40 seconds
#endregion
#region Ctor
// initializes a new instance of MainDom
public MainDom(ILogger<MainDom> logger, IMainDomLock systemLock)
{
_logger = logger;
_mainDomLock = systemLock;
}
#endregion
public bool Acquire(IApplicationShutdownRegistry hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
return LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () =>
{
hostingEnvironment.RegisterObject(this);
return Acquire();
});
}
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
/// <param name="release">An action to execute before the AppDomain releases the main domain status.</param>
/// <param name="weight">An optional weight (lower goes first).</param>
/// <returns>A value indicating whether it was possible to register.</returns>
public bool Register(Action release, int weight = 100)
=> Register(null, release, weight);
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
/// <param name="install">An action to execute when registering.</param>
/// <param name="release">An action to execute before the AppDomain releases the main domain status.</param>
/// <param name="weight">An optional weight (lower goes first).</param>
/// <returns>A value indicating whether it was possible to register.</returns>
/// <remarks>If registering is successful, then the <paramref name="install"/> action
/// is guaranteed to execute before the AppDomain releases the main domain status.</remarks>
public bool Register(Action install, Action release, int weight = 100)
{
lock (_locko)
{
if (_signaled) return false;
if (_isMainDom == false)
{
_logger.LogWarning("Register called when MainDom has not been acquired");
return false;
}
install?.Invoke();
if (release != null)
_callbacks.Add(new KeyValuePair<int, Action>(weight, release));
return true;
}
}
// handles the signal requesting that the main domain is released
private void OnSignal(string source)
{
// once signaled, we stop waiting, but then there is the hosting environment
// so we have to make sure that we only enter that method once
lock (_locko)
{
_logger.LogDebug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source);
if (_signaled) return;
if (_isMainDom == false) return; // probably not needed
_signaled = true;
try
{
_logger.LogInformation("Stopping ({SignalSource})", source);
foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value))
{
try
{
callback(); // no timeout on callbacks
}
catch (Exception e)
{
_logger.LogError(e, "Error while running callback");
continue;
}
}
_logger.LogDebug("Stopped ({SignalSource})", source);
}
finally
{
// in any case...
_isMainDom = false;
_mainDomLock.Dispose();
_logger.LogInformation("Released ({SignalSource})", source);
}
}
}
// acquires the main domain
private bool Acquire()
{
// if signaled, too late to acquire, give up
// the handler is not installed so that would be the hosting environment
if (_signaled)
{
_logger.LogInformation("Cannot acquire (signaled).");
return false;
}
_logger.LogInformation("Acquiring.");
// Get the lock
var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult();
if (!acquired)
{
_logger.LogInformation("Cannot acquire (timeout).");
// In previous versions we'd let a TimeoutException be thrown
// and the appdomain would not start. We have the opportunity to allow it to
// start without having MainDom? This would mean that it couldn't write
// to nucache/examine and would only be ok if this was a super short lived appdomain.
// maybe safer to just keep throwing in this case.
throw new TimeoutException("Cannot acquire MainDom");
// return false;
}
try
{
// Listen for the signal from another AppDomain coming online to release the lock
_mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal"));
}
catch (OperationCanceledException ex)
{
// the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception
_logger.LogWarning(ex, ex.Message);
}
_logger.LogInformation("Acquired.");
return true;
}
/// <summary>
/// Gets a value indicating whether the current domain is the main domain.
/// </summary>
/// <remarks>
/// Acquire must be called first else this will always return false
/// </remarks>
public bool IsMainDom => _isMainDom;
// IRegisteredObject
void IRegisteredObject.Stop(bool immediate)
{
OnSignal("environment"); // will run once
// The web app is stopping, need to wind down
Dispose(true);
_hostingEnvironment?.UnregisterObject(this);
}
#region IDisposable Support
// This code added to correctly implement the disposable pattern.
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_mainDomLock.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
public static string GetMainDomId(IHostingEnvironment hostingEnvironment)
{
// HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
var appId = hostingEnvironment.ApplicationId?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty;
// combining with the physical path because if running on eg IIS Express,
// two sites could have the same appId even though they are different.
//
// now what could still collide is... two sites, running in two different processes
// and having the same appId, and running on the same app physical path
//
// we *cannot* use the process ID here because when an AppPool restarts it is
// a new process for the same application path
var appPath = hostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
var hash = (appId + ":::" + appPath).GenerateHash<SHA1>();
return hash;
}
}
}