// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web.Caching;
using System.Web.Compilation;
using System.Web.Configuration;
using System.Web.WebPages.Deployment.Resources;
using Microsoft.Internal.Web.Utils;
using Microsoft.Web.Infrastructure;
namespace System.Web.WebPages.Deployment
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static class PreApplicationStartCode
{
///
/// Key used to indicate to tooling that the compile exception we throw to refresh the app domain originated from us so that they can deal with it correctly.
///
private const string ToolingIndicatorKey = "WebPages.VersionChange";
// NOTE: Do not add public fields, methods, or other members to this class.
// This class does not show up in Intellisense so members on it will not be
// discoverable by users. Place new members on more appropriate classes that
// relate to the public API (for example, a LoginUrl property should go on a
// membership-related class).
private static readonly IFileSystem _physicalFileSystem = new PhysicalFileSystem();
private static bool _startWasCalled;
public static void Start()
{
// Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
// another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
// order so we have to guard against multiple calls.
// All Start calls are made on same thread, so no lock needed here.
if (_startWasCalled)
{
return;
}
_startWasCalled = true;
StartCore();
}
internal static bool StartCore()
{
var buildManager = new BuildManagerWrapper();
NameValueCollection appSettings = WebConfigurationManager.AppSettings;
Action loadWebPages = LoadWebPages;
Action registerForChangeNotification = RegisterForChangeNotifications;
IEnumerable loadedAssemblies = AssemblyUtils.GetLoadedAssemblies();
return StartCore(_physicalFileSystem, HttpRuntime.AppDomainAppPath, HttpRuntime.BinDirectory, appSettings, loadedAssemblies,
buildManager, loadWebPages, registerForChangeNotification);
}
// Adds Parameter for unit tests
internal static bool StartCore(IFileSystem fileSystem, string appDomainAppPath, string binDirectory, NameValueCollection appSettings, IEnumerable loadedAssemblies,
IBuildManager buildManager, Action loadWebPages, Action registerForChangeNotification, Func getAssemblyNameThunk = null)
{
if (WebPagesDeployment.IsExplicitlyDisabled(appSettings))
{
// If WebPages is explicitly disabled, exit.
Debug.WriteLine("WebPages Bootstrapper v{0}: not loading WebPages since it is disabled", AssemblyUtils.ThisAssemblyName.Version);
return false;
}
Version maxWebPagesVersion = AssemblyUtils.GetMaxWebPagesVersion(loadedAssemblies);
Debug.Assert(maxWebPagesVersion != null, "Function must return some max value.");
if (AssemblyUtils.ThisAssemblyName.Version != maxWebPagesVersion)
{
// Always let the highest version determine what needs to be done. This would make future proofing simpler.
Debug.WriteLine("WebPages Bootstrapper v{0}: Higher version v{1} is available.", AssemblyUtils.ThisAssemblyName.Version, maxWebPagesVersion);
return false;
}
var webPagesEnabled = WebPagesDeployment.IsEnabled(fileSystem, appDomainAppPath, appSettings);
Version binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssemblyNameThunk);
Version version = WebPagesDeployment.GetVersionInternal(appSettings, binVersion, defaultVersion: maxWebPagesVersion);
// Asserts to ensure unit tests are set up correctly. So essentially, we're unit testing the unit tests.
Debug.Assert(version != null, "GetVersion always returns a version");
Debug.Assert(binVersion == null || binVersion <= maxWebPagesVersion, "binVersion cannot be higher than max version");
if ((binVersion != null) && (binVersion != version))
{
// Determine if there's a version conflict. A conflict could occur if there's a version specified in the bin which is different from the version specified in the
// config that is different.
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ConfigurationResources.WebPagesVersionConflict, version, binVersion));
}
else if (binVersion != null)
{
// The rest of the code is only meant to be executed if we are executing from the GAC.
// If a version is bin deployed, we don't need to do anything special to bootstrap.
return false;
}
else if (!webPagesEnabled)
{
Debug.WriteLine("WebPages Bootstrapper v{0}: WebPages not enabled, registering for change notifications", AssemblyUtils.ThisAssemblyName.Version);
// Register for change notifications under the application root
registerForChangeNotification();
return false;
}
else if (!AssemblyUtils.IsVersionAvailable(loadedAssemblies, version))
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ConfigurationResources.WebPagesVersionNotFound, version, AssemblyUtils.ThisAssemblyName.Version));
}
Debug.WriteLine("WebPages Bootstrapper v{0}: loading version {1}, loading WebPages", AssemblyUtils.ThisAssemblyName.Version, version);
// If the version the application was compiled earlier was different, invalidate compilation results by adding a file to the bin.
InvalidateCompilationResultsIfVersionChanged(buildManager, fileSystem, binDirectory, version);
loadWebPages(version);
return true;
}
///
/// WebPages stores the version to be compiled against in AppSettings as >add key="webpages:version" value="1.0" /<.
/// Changing values AppSettings does not cause recompilation therefore we could run into a state where we have files compiled against v1 but the application is
/// currently v2.
///
private static void InvalidateCompilationResultsIfVersionChanged(IBuildManager buildManager, IFileSystem fileSystem, string binDirectory, Version currentVersion)
{
Version previousVersion = WebPagesDeployment.GetPreviousRuntimeVersion(buildManager);
// Persist the current version number in BuildManager's cached file
WebPagesDeployment.PersistRuntimeVersion(buildManager, currentVersion);
if (previousVersion == null)
{
// Do nothing.
}
else if (previousVersion != currentVersion)
{
// If the previous runtime version is different, perturb the bin directory so that it forces recompilation.
WebPagesDeployment.ForceRecompile(fileSystem, binDirectory);
var httpCompileException = new HttpCompileException(ConfigurationResources.WebPagesVersionChanges);
// Indicator for tooling
httpCompileException.Data[ToolingIndicatorKey] = true;
throw httpCompileException;
}
}
// Copied from xsp\System\Web\Compilation\BuildManager.cs
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Copied from System.Web.dll")]
internal static ICollection GetPreStartInitMethodsFromAssemblyCollection(IEnumerable assemblies)
{
List methods = new List();
foreach (Assembly assembly in assemblies)
{
PreApplicationStartMethodAttribute[] attributes = null;
try
{
attributes = (PreApplicationStartMethodAttribute[])assembly.GetCustomAttributes(typeof(PreApplicationStartMethodAttribute), inherit: true);
}
catch
{
// GetCustomAttributes invokes the constructors of the attributes, so it is possible that they might throw unexpected exceptions.
// (Dev10 bug 831981)
}
if (attributes != null && attributes.Length != 0)
{
Debug.Assert(attributes.Length == 1);
PreApplicationStartMethodAttribute attribute = attributes[0];
Debug.Assert(attribute != null);
MethodInfo method = null;
// Ensure the Type on the attribute is in the same assembly as the attribute itself
if (attribute.Type != null && !String.IsNullOrEmpty(attribute.MethodName) && attribute.Type.Assembly == assembly)
{
method = FindPreStartInitMethod(attribute.Type, attribute.MethodName);
}
if (method != null)
{
methods.Add(method);
}
// No-op if the attribute is invalid
/*
else {
throw new HttpException(SR.GetString(SR.Invalid_PreApplicationStartMethodAttribute_value,
assembly.FullName,
(attribute.Type != null ? attribute.Type.FullName : String.Empty),
attribute.MethodName));
}
*/
}
}
return methods;
}
// Copied from xsp\System\Web\Compilation\BuildManager.cs
internal static MethodInfo FindPreStartInitMethod(Type type, string methodName)
{
Debug.Assert(type != null);
Debug.Assert(!String.IsNullOrEmpty(methodName));
MethodInfo method = null;
if (type.IsPublic)
{
// Verify that type is public to avoid allowing internal code execution. This implementation will not match
// nested public types.
method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
binder: null,
types: Type.EmptyTypes,
modifiers: null);
}
return method;
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The cache disposes of the dependency")]
private static void RegisterForChangeNotifications()
{
string physicalPath = HttpRuntime.AppDomainAppPath;
CacheDependency cacheDependency = new CacheDependency(physicalPath, DateTime.UtcNow);
var key = WebPagesDeployment.CacheKeyPrefix + physicalPath;
HttpRuntime.Cache.Insert(key, physicalPath, cacheDependency,
Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(OnChanged));
}
private static void OnChanged(string key, object value, CacheItemRemovedReason reason)
{
// Only handle case when the dependency has changed.
if (reason != CacheItemRemovedReason.DependencyChanged)
{
return;
}
// Scan the app root for a webpages file
if (WebPagesDeployment.AppRootContainsWebPagesFile(_physicalFileSystem, HttpRuntime.AppDomainAppPath))
{
// Unload the app domain so we register plan9 when the app restarts
InfrastructureHelper.UnloadAppDomain();
}
else
{
// We need to re-register since the item was removed from the cache
RegisterForChangeNotifications();
}
}
private static void LoadWebPages(Version version)
{
IEnumerable assemblyList = AssemblyUtils.GetAssembliesForVersion(version);
var assemblies = assemblyList.Select(LoadAssembly);
foreach (var asm in assemblies)
{
BuildManager.AddReferencedAssembly(asm);
}
foreach (var m in GetPreStartInitMethodsFromAssemblyCollection(assemblies))
{
m.Invoke(null, null);
}
}
private static Assembly LoadAssembly(AssemblyName name)
{
return Assembly.Load(name);
}
}
}