//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ /************************************************************************************************************/ namespace System.Web.Compilation { using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Remoting; using System.Security.Permissions; using System.Threading; using System.Web; using System.Web.Hosting; using System.Web.Util; using Debug = System.Web.Util.Debug; // Flags that drive the behavior of precompilation [Flags] public enum PrecompilationFlags { Default = 0x00000000, // determines whether the deployed app will be updatable Updatable = 0x00000001, // determines whether the target directory can be overwritten OverwriteTarget = 0x00000002, // determines whether the compiler will emit debug information ForceDebug = 0x00000004, // determines whether the application is built clean Clean = 0x00000008, // determines whether the /define:CodeAnalysis flag needs to be added // as compilation symbol CodeAnalysis = 0x00000010, // determines whether to generate APTCA attribute. AllowPartiallyTrustedCallers = 0x00000020, // determines whether to delaySign the generate assemblies. DelaySign = 0x00000040, // determines whether to use fixed assembly names FixedNames = 0x00000080, // determines whether to skip BadImageFormatException IgnoreBadImageFormatException = 0x00000100, } [Serializable] public class ClientBuildManagerParameter { private string _strongNameKeyFile; private string _strongNameKeyContainer; private PrecompilationFlags _precompilationFlags = PrecompilationFlags.Default; private List _excludedVirtualPaths; public List ExcludedVirtualPaths { get { if (_excludedVirtualPaths == null) { _excludedVirtualPaths = new List(); } return _excludedVirtualPaths; } } // Determines the behavior of the precompilation public PrecompilationFlags PrecompilationFlags { get { return _precompilationFlags; } set { _precompilationFlags = value; } } public string StrongNameKeyFile { get { return _strongNameKeyFile; } set { _strongNameKeyFile = value; } } public string StrongNameKeyContainer { get { return _strongNameKeyContainer; } set { _strongNameKeyContainer = value; } } } // // This class provide access to the BuildManager outside of an IIS environment // Instances of this class are created in the caller's App Domain. // // It creates and configures the new App Domain for handling BuildManager calls // using System.Web.Hosting.ApplicationHost.CreateApplicationHost() // [PermissionSet(SecurityAction.LinkDemand, Unrestricted = true)] [PermissionSet(SecurityAction.InheritanceDemand, Unrestricted = true)] public sealed class ClientBuildManager : MarshalByRefObject, IDisposable { private VirtualPath _virtualPath; private string _physicalPath; private string _installPath; private string _appId; private IApplicationHost _appHost; private string _codeGenDir; private HostingEnvironmentParameters _hostingParameters; private ClientBuildManagerTypeDescriptionProviderBridge _cbmTdpBridge; private WaitCallback _onAppDomainUnloadedCallback; private WaitCallback _onAppDomainShutdown; private ApplicationShutdownReason _reason; private BuildManagerHost _host; private Exception _hostCreationException; private bool _hostCreationPending; public event BuildManagerHostUnloadEventHandler AppDomainUnloaded; public event EventHandler AppDomainStarted; public event BuildManagerHostUnloadEventHandler AppDomainShutdown; // internal lock used for host creation. private object _lock = new object(); // Whether to wait for the call back from the previous host unloading before creating a new one private bool _waitForCallBack; private const string IISExpressPrefix = "/IISExpress/"; /* * Creates an instance of the ClientBuildManager. * appPhysicalSourceDir points to the physical root of the application (e.g "c:\myapp") * virtualPath is the virtual path to the app root. It can be anything (e.g. "/dummy"), * but ideally it should match the path later given to Cassini, in order for * compilation that happens here to be reused there. */ public ClientBuildManager(string appVirtualDir, string appPhysicalSourceDir) : this(appVirtualDir, appPhysicalSourceDir, appPhysicalTargetDir: null, parameter: null) { } /* * Creates an instance of the PrecompilationManager. * appPhysicalSourceDir points to the physical root of the application (e.g "c:\myapp") * appVirtualDir is the virtual path to the app root. It can be anything (e.g. "/dummy"), * but ideally it should match the path later given to Cassini, in order for * compilation that happens here to be reused there. * appPhysicalTargetDir is the directory where the precompiled site is placed */ public ClientBuildManager(string appVirtualDir, string appPhysicalSourceDir, string appPhysicalTargetDir) : this(appVirtualDir, appPhysicalSourceDir, appPhysicalTargetDir, parameter: null) { } /* * Creates an instance of the PrecompilationManager. * appPhysicalSourceDir points to the physical root of the application (e.g "c:\myapp") * appVirtualDir is the virtual path to the app root. It can be anything (e.g. "/dummy"), * but ideally it should match the path later given to Cassini, in order for * compilation that happens here to be reused there. * appPhysicalTargetDir is the directory where the precompiled site is placed * flags determines the behavior of the precompilation */ public ClientBuildManager(string appVirtualDir, string appPhysicalSourceDir, string appPhysicalTargetDir, ClientBuildManagerParameter parameter) : this(appVirtualDir, appPhysicalSourceDir, appPhysicalTargetDir, parameter, typeDescriptionProvider: null) { } /* * Creates an instance of the PrecompilationManager. * appPhysicalSourceDir points to the physical root of the application (e.g "c:\myapp") * appVirtualDir is the virtual path to the app root. It can be anything (e.g. "/dummy"), * but ideally it should match the path later given to Cassini, in order for * compilation that happens here to be reused there. * appPhysicalTargetDir is the directory where the precompiled site is placed * typeDescriptionProvider is the provider used for retrieving type * information for multi-targeting */ public ClientBuildManager(string appVirtualDir, string appPhysicalSourceDir, string appPhysicalTargetDir, ClientBuildManagerParameter parameter, TypeDescriptionProvider typeDescriptionProvider) { if (parameter == null) { parameter = new ClientBuildManagerParameter(); } InitializeCBMTDPBridge(typeDescriptionProvider); // Always build clean in precompilation for deployment mode, // since building incrementally raises all kind of issues (VSWhidbey 382954). if (!String.IsNullOrEmpty(appPhysicalTargetDir)) { parameter.PrecompilationFlags |= PrecompilationFlags.Clean; } _hostingParameters = new HostingEnvironmentParameters(); _hostingParameters.HostingFlags = HostingEnvironmentFlags.DontCallAppInitialize | HostingEnvironmentFlags.ClientBuildManager; _hostingParameters.ClientBuildManagerParameter = parameter; _hostingParameters.PrecompilationTargetPhysicalDirectory = appPhysicalTargetDir; if (typeDescriptionProvider != null) { _hostingParameters.HostingFlags |= HostingEnvironmentFlags.SupportsMultiTargeting; } // Make sure the app virtual dir starts with / if (appVirtualDir[0] != '/') appVirtualDir = "/" + appVirtualDir; if (appPhysicalSourceDir == null && appVirtualDir.StartsWith(IISExpressPrefix, StringComparison.OrdinalIgnoreCase) && appVirtualDir.Length > IISExpressPrefix.Length) { // appVirtualDir should have the form "/IISExpress//LM/W3SVC/", // and we will try to extract the version. The version will be validated // when it is passed to IISVersionHelper..ctor. int endSlash = appVirtualDir.IndexOf('/', IISExpressPrefix.Length); if (endSlash > 0) { _hostingParameters.IISExpressVersion = appVirtualDir.Substring(IISExpressPrefix.Length, endSlash - IISExpressPrefix.Length); appVirtualDir = appVirtualDir.Substring(endSlash); } } Initialize(VirtualPath.CreateNonRelative(appVirtualDir), appPhysicalSourceDir); } /* * returns the codegendir used by runtime appdomain */ public string CodeGenDir { get { if (_codeGenDir == null) { EnsureHostCreated(); _codeGenDir = _host.CodeGenDir; } return _codeGenDir; } } /* * Indicates whether the host is created. */ public bool IsHostCreated { get { return _host != null; } } /* * Create an object in the runtime appdomain */ public IRegisteredObject CreateObject(Type type, bool failIfExists) { if (type == null) { throw new ArgumentNullException("type"); } EnsureHostCreated(); Debug.Assert(_appId != null); Debug.Assert(_appHost != null); _host.RegisterAssembly(type.Assembly.FullName, type.Assembly.Location); ApplicationManager appManager = ApplicationManager.GetApplicationManager(); return appManager.CreateObjectInternal(_appId, type, _appHost, failIfExists, _hostingParameters); } /* * Return the list of directories that would cause appdomain shutdown. */ public string[] GetAppDomainShutdownDirectories() { Debug.Trace("CBM", "GetAppDomainShutdownDirectories"); return FileChangesMonitor.s_dirsToMonitor; } /* * Makes sure that all the top level files are compiled (code, global.asax, ...) */ public void CompileApplicationDependencies() { Debug.Trace("CBM", "CompileApplicationDependencies"); EnsureHostCreated(); _host.CompileApplicationDependencies(); } public IDictionary GetBrowserDefinitions() { Debug.Trace("CBM", "GetBrowserDefinitions"); EnsureHostCreated(); return _host.GetBrowserDefinitions(); } /* * Returns the physical path of the generated file corresponding to the virtual directory. * Note the virtualPath needs to use this format: * "/[appname]/App_WebReferences/{[subDir]/}" */ public string GetGeneratedSourceFile(string virtualPath) { Debug.Trace("CBM", "GetGeneratedSourceFile " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); return _host.GetGeneratedSourceFile(VirtualPath.CreateTrailingSlash(virtualPath)); } /* * Returns the virtual path of the corresponding generated file. * Note the filepath needs to be a full path. */ public string GetGeneratedFileVirtualPath(string filePath) { Debug.Trace("CBM", "GetGeneratedFileVirtualPath " + filePath); if (filePath == null) { throw new ArgumentNullException("filePath"); } EnsureHostCreated(); return _host.GetGeneratedFileVirtualPath(filePath); } /* * Returns an array of the virtual paths to all the code directories in the app thru the hosted appdomain */ public string[] GetVirtualCodeDirectories() { Debug.Trace("CBM", "GetHostedVirtualCodeDirectories"); EnsureHostCreated(); return _host.GetVirtualCodeDirectories(); } /* * Returns an array of the assemblies defined in the bin and assembly reference config section */ public String[] GetTopLevelAssemblyReferences(string virtualPath) { Debug.Trace("CBM", "GetHostedVirtualCodeDirectories"); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); return _host.GetTopLevelAssemblyReferences(VirtualPath.Create(virtualPath)); } /* * Returns the compiler type and parameters that need to be used to build * a given code directory. Also, returns the directory containing all the code * files generated from non-code files in the code directory (e.g. wsdl files) */ public void GetCodeDirectoryInformation(string virtualCodeDir, out Type codeDomProviderType, out CompilerParameters compilerParameters, out string generatedFilesDir) { Debug.Trace("CBM", "GetCodeDirectoryInformation " + virtualCodeDir); if (virtualCodeDir == null) { throw new ArgumentNullException("virtualCodeDir"); } EnsureHostCreated(); _host.GetCodeDirectoryInformation(VirtualPath.CreateTrailingSlash(virtualCodeDir), out codeDomProviderType, out compilerParameters, out generatedFilesDir); Debug.Trace("CBM", "GetCodeDirectoryInformation " + virtualCodeDir + " end"); } /* * Returns the compiler type and parameters that need to be used to build * a given file. */ public void GetCompilerParameters(string virtualPath, out Type codeDomProviderType, out CompilerParameters compilerParameters) { Debug.Trace("CBM", "GetCompilerParameters " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); _host.GetCompilerParams(VirtualPath.Create(virtualPath), out codeDomProviderType, out compilerParameters); } /* * Returns the codedom tree and the compiler type/param for a given file. */ public CodeCompileUnit GenerateCodeCompileUnit( string virtualPath, out Type codeDomProviderType, out CompilerParameters compilerParameters, out IDictionary linePragmasTable) { Debug.Trace("CBM", "GenerateCodeCompileUnit " + virtualPath); return GenerateCodeCompileUnit(virtualPath, null, out codeDomProviderType, out compilerParameters, out linePragmasTable); } public CodeCompileUnit GenerateCodeCompileUnit( string virtualPath, String virtualFileString, out Type codeDomProviderType, out CompilerParameters compilerParameters, out IDictionary linePragmasTable) { Debug.Trace("CBM", "GenerateCodeCompileUnit " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); return _host.GenerateCodeCompileUnit(VirtualPath.Create(virtualPath), virtualFileString, out codeDomProviderType, out compilerParameters, out linePragmasTable); } public string GenerateCode( string virtualPath, String virtualFileString, out IDictionary linePragmasTable) { Debug.Trace("CBM", "GenerateCode " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); return _host.GenerateCode(VirtualPath.Create(virtualPath), virtualFileString, out linePragmasTable); } /* * Returns the compiled type for an input file */ public Type GetCompiledType(string virtualPath) { Debug.Trace("CBM", "GetCompiledType " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } EnsureHostCreated(); string[] typeAndAsemblyName = _host.GetCompiledTypeAndAssemblyName(VirtualPath.Create(virtualPath), null); if (typeAndAsemblyName == null) return null; Assembly a = Assembly.LoadFrom(typeAndAsemblyName[1]); Type t = a.GetType(typeAndAsemblyName[0]); return t; } /* * Compile a file */ public void CompileFile(string virtualPath) { CompileFile(virtualPath, null); } public void CompileFile(string virtualPath, ClientBuildManagerCallback callback) { Debug.Trace("CBM", "CompileFile " + virtualPath); if (virtualPath == null) { throw new ArgumentNullException("virtualPath"); } try { EnsureHostCreated(); _host.GetCompiledTypeAndAssemblyName(VirtualPath.Create(virtualPath), callback); } finally { // DevDiv 180798. We are returning null in ClientBuildManagerCallback.InitializeLifetimeService, // so we need to manually disconnect the instance so that it will be released. if (callback != null) { RemotingServices.Disconnect(callback); } } } /* * Indicates whether an assembly is a code assembly. */ public bool IsCodeAssembly(string assemblyName) { Debug.Trace("CBM", "IsCodeAssembly " + assemblyName); if (assemblyName == null) { throw new ArgumentNullException("assemblyName"); } // EnsureHostCreated(); bool result = _host.IsCodeAssembly(assemblyName); Debug.Trace("CBM", "IsCodeAssembly " + result.ToString()); return result; } public bool Unload() { Debug.Trace("CBM", "Unload"); BuildManagerHost host = _host; if (host != null) { _host = null; return host.UnloadAppDomain(); } return false; } /* * Precompile an application */ public void PrecompileApplication() { PrecompileApplication(null); } /* * Precompile an application with callback support */ public void PrecompileApplication(ClientBuildManagerCallback callback) { PrecompileApplication(callback, false); } public void PrecompileApplication(ClientBuildManagerCallback callback, bool forceCleanBuild) { Debug.Trace("CBM", "PrecompileApplication"); PrecompilationFlags savedFlags = _hostingParameters.ClientBuildManagerParameter.PrecompilationFlags; if (forceCleanBuild) { // If there was a previous host, it will be unloaded by CBM and we will wait for the callback. // If there was no previous host, we don't do any waiting. // DevDiv 46290 _waitForCallBack = _host != null; Debug.Trace("CBM", "Started Unload"); // Unload the existing appdomain so the new one will be created with the clean flag Unload(); _hostingParameters.ClientBuildManagerParameter.PrecompilationFlags = savedFlags | PrecompilationFlags.Clean; WaitForCallBack(); } try { EnsureHostCreated(); _host.PrecompileApp(callback, _hostingParameters.ClientBuildManagerParameter.ExcludedVirtualPaths); } finally { if (forceCleanBuild) { // Revert precompilationFlags _hostingParameters.ClientBuildManagerParameter.PrecompilationFlags = savedFlags; } // DevDiv 180798. We are returning null in ClientBuildManagerCallback.InitializeLifetimeService, // so we need to manually disconnect the instance so that it will be released. if (callback != null) { RemotingServices.Disconnect(callback); } } } // _waitForCallBack is set to false in OnAppDomainUnloaded. // This method waits until it is set to false before continuing, so that // we do not run into a concurrency issue where _host could be set to null. // DevDiv 46290 private void WaitForCallBack() { Debug.Trace("CBM", "WaitForCallBack"); int waited = 0; while (_waitForCallBack && waited <= 50) { Thread.Sleep(200); waited++; } if (_waitForCallBack) { Debug.Trace("CBM", "timeout while waiting for callback"); } else { Debug.Trace("CBM", "callback received before timeout"); } } public override Object InitializeLifetimeService() { return null; // never expire lease } internal void Initialize(VirtualPath virtualPath, string physicalPath) { Debug.Trace("CBM", "Initialize"); _virtualPath = virtualPath; _physicalPath = FileUtil.FixUpPhysicalDirectory(physicalPath); _onAppDomainUnloadedCallback = new WaitCallback(OnAppDomainUnloadedCallback); _onAppDomainShutdown = new WaitCallback(OnAppDomainShutdownCallback); _installPath = RuntimeEnvironment.GetRuntimeDirectory(); // Do not create host during intialization. It will be done on demand. //CreateHost(); } private void EnsureHostCreated() { if (_host == null) { lock (_lock) { // Create the host if necessary if (_host == null) { CreateHost(); Debug.Trace("CBM", "EnsureHostCreated: after CreateHost()"); } } } // If an exception happened during host creation, rethrow it if (_hostCreationException != null) { Debug.Trace("CBM", "EnsureHostCreated: failed. " + _hostCreationException); // We need to wrap it in a new exception, otherwise we lose the original stack. throw new HttpException(_hostCreationException.Message, _hostCreationException); } } private void CreateHost() { Debug.Trace("CBM", "CreateHost"); Debug.Assert(_host == null); Debug.Assert(!_hostCreationPending, "CreateHost: creation already pending"); _hostCreationPending = true; // Use a local to avoid having a partially created _host BuildManagerHost host = null; try { string appId; IApplicationHost appHost; ApplicationManager appManager = ApplicationManager.GetApplicationManager(); host = (BuildManagerHost) appManager.CreateObjectWithDefaultAppHostAndAppId( _physicalPath, _virtualPath, typeof(BuildManagerHost), false /*failIfExists*/, _hostingParameters, out appId, out appHost); // host appdomain cannot be unloaded during creation. host.AddPendingCall(); host.Configure(this); _host = host; _appId = appId; _appHost = appHost; _hostCreationException = _host.InitializationException; } catch (Exception e) { // If an exception happens, keep track of it _hostCreationException = e; // Even though the host initialization failed, keep track of it so subsequent // request will see the error _host = host; } finally { _hostCreationPending = false; if (host != null) { // Notify the client that the host is ready if (AppDomainStarted != null) { AppDomainStarted(this, EventArgs.Empty); } // The host can be unloaded safely now. host.RemovePendingCall(); } } Debug.Trace("CBM", "CreateHost LEAVE"); } // Called by BuildManagerHost when the ASP appdomain is unloaded internal void OnAppDomainUnloaded(ApplicationShutdownReason reason) { Debug.Trace("CBM", "OnAppDomainUnloaded " + reason.ToString()); _reason = reason; _waitForCallBack = false; // Don't do anything that can be slow here. Instead queue in a worker thread ThreadPool.QueueUserWorkItem(_onAppDomainUnloadedCallback); } internal void ResetHost() { lock (_lock) { // Though _appId and _appHost are created along with _host, // we need not reset those here as they always correspond to // default app id and app host. _host = null; _hostCreationException = null; } } [PermissionSet(SecurityAction.Assert, Unrestricted = true)] private void OnAppDomainUnloadedCallback(Object unused) { Debug.Trace("CBM", "OnAppDomainUnloadedCallback"); // Notify the client that the appdomain is unloaded if (AppDomainUnloaded != null) { AppDomainUnloaded(this, new BuildManagerHostUnloadEventArgs(_reason)); } } [PermissionSet(SecurityAction.Assert, Unrestricted = true)] private void OnAppDomainShutdownCallback(Object o) { if (AppDomainShutdown != null) { AppDomainShutdown(this, new BuildManagerHostUnloadEventArgs((ApplicationShutdownReason)o)); } } internal void OnAppDomainShutdown(ApplicationShutdownReason reason) { // Don't do anything that can be slow here. Instead queue in a worker thread ThreadPool.QueueUserWorkItem(_onAppDomainShutdown, reason); } private void InitializeCBMTDPBridge(TypeDescriptionProvider typeDescriptionProvider) { if (typeDescriptionProvider == null){ return; } _cbmTdpBridge = new ClientBuildManagerTypeDescriptionProviderBridge(typeDescriptionProvider); } internal ClientBuildManagerTypeDescriptionProviderBridge CBMTypeDescriptionProviderBridge { get { return _cbmTdpBridge; } } #region IDisposable //Dispose the runtime appdomain properly when CBM is disposed void IDisposable.Dispose() { Unload(); } #endregion } [PermissionSet(SecurityAction.LinkDemand, Unrestricted = true)] [PermissionSet(SecurityAction.InheritanceDemand, Unrestricted = true)] public class BuildManagerHostUnloadEventArgs : EventArgs { ApplicationShutdownReason _reason; public BuildManagerHostUnloadEventArgs(ApplicationShutdownReason reason) { _reason = reason; } // Get the reason for the hosted appdomain shutdown public ApplicationShutdownReason Reason { get { return _reason; } } } public delegate void BuildManagerHostUnloadEventHandler(object sender, BuildManagerHostUnloadEventArgs e); /* * Type of the entries in the table returned by GenerateCodeCompileUnit */ [Serializable] public sealed class LinePragmaCodeInfo { public LinePragmaCodeInfo() { } public LinePragmaCodeInfo(int startLine, int startColumn, int startGeneratedColumn, int codeLength, bool isCodeNugget) { this._startLine = startLine; this._startColumn = startColumn; this._startGeneratedColumn = startGeneratedColumn; this._codeLength = codeLength; this._isCodeNugget = isCodeNugget; } // Starting line in ASPX file internal int _startLine; public int StartLine { get { return _startLine; } } // Starting column in the ASPX file internal int _startColumn; public int StartColumn { get { return _startColumn; } } // Starting column in the generated source file (assuming no indentations are used) internal int _startGeneratedColumn; public int StartGeneratedColumn { get { return _startGeneratedColumn; } } // Length of the code snippet internal int _codeLength; public int CodeLength { get { return _codeLength; } } // Whether the script block is a nugget. internal bool _isCodeNugget; public bool IsCodeNugget { get { return _isCodeNugget; } } } }