1 //------------------------------------------------------------------------------
2 // <copyright file="BuildResultCache.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
9 /*********************************
12 MemoryBuildResultCache
14 StandardDiskBuildResultCache
15 PrecompBaseDiskBuildResultCache
16 PrecompilerDiskBuildResultCache
17 PrecompiledSiteDiskBuildResultCache
19 **********************************/
21 namespace System.Web.Compilation {
25 using System.Collections;
26 using System.Diagnostics.CodeAnalysis;
27 using System.Globalization;
29 using System.Threading;
30 using System.Reflection;
31 using System.Security.Permissions;
32 using System.Web.Hosting;
33 using System.Web.Util;
34 using System.Web.Caching;
37 internal abstract class BuildResultCache {
38 internal BuildResult GetBuildResult(string cacheKey) {
39 return GetBuildResult(cacheKey, null /*virtualPath*/, 0 /*hashCode*/);
42 internal abstract BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate=true);
44 internal void CacheBuildResult(string cacheKey, BuildResult result, DateTime utcStart) {
45 CacheBuildResult(cacheKey, result, 0 /*hashCode*/, utcStart);
48 internal abstract void CacheBuildResult(string cacheKey, BuildResult result,
49 long hashCode, DateTime utcStart);
51 internal static string GetAssemblyCacheKey(string assemblyPath) {
52 string assemblyName = Util.GetAssemblyNameFromFileName(Path.GetFileName(assemblyPath));
53 return GetAssemblyCacheKeyFromName(assemblyName);
56 internal static string GetAssemblyCacheKey(Assembly assembly) {
57 Debug.Assert(!assembly.GlobalAssemblyCache);
58 return GetAssemblyCacheKeyFromName(assembly.GetName().Name);
61 internal static string GetAssemblyCacheKeyFromName(string assemblyName) {
62 Debug.Assert(StringUtil.StringStartsWith(assemblyName, BuildManager.AssemblyNamePrefix));
63 return CacheInternal.PrefixAssemblyPath + assemblyName.ToLowerInvariant();
68 internal class MemoryBuildResultCache: BuildResultCache {
70 private CacheItemRemovedCallback _onRemoveCallback;
72 // The keys are simple assembly names
73 // The values are ArrayLists containing the simple names of assemblies that depend on it
74 private Hashtable _dependentAssemblies = new Hashtable();
76 internal MemoryBuildResultCache() {
78 // Register an AssemblyLoad event
79 AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(OnAssemblyLoad);
82 [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
83 private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args) {
84 Assembly a = args.LoadedAssembly;
86 // Ignore GAC assemblies
87 if (a.GlobalAssemblyCache)
90 // Ignore assemblies that don't start with our prefix
91 string name = a.GetName().Name;
92 if (!StringUtil.StringStartsWith(name, BuildManager.AssemblyNamePrefix))
95 // Go through all the assemblies it references
96 foreach (AssemblyName assemblyName in a.GetReferencedAssemblies()) {
98 // Ignore references that don't start with our prefix
99 if (!StringUtil.StringStartsWith(assemblyName.Name, BuildManager.AssemblyNamePrefix))
102 lock (_dependentAssemblies) {
103 // Check whether we already have an ArrayList for this reference
104 ArrayList dependentList = _dependentAssemblies[assemblyName.Name] as ArrayList;
105 if (dependentList == null) {
106 // If not, create one and add it to the hashtable
107 dependentList = new ArrayList();
108 _dependentAssemblies[assemblyName.Name] = dependentList;
111 // Add the assembly that just got loaded as a dependent
112 Debug.Assert(!dependentList.Contains(name));
113 dependentList.Add(name);
118 internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) {
119 Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the memory cache");
121 string key = GetMemoryCacheKey(cacheKey);
122 BuildResult result = (BuildResult) HttpRuntime.Cache.InternalCache.Get(key);
124 // Not found in the cache
125 if (result == null) {
126 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the memory cache");
130 // We found it in the cache, but is it up to date. First, if it uses a CacheDependency,
131 // it must be up to date (this is the default case when using MapPathBasedVirtualPathProvider).
132 // If not, then we need to explicitely check that it's up to date (more expensive)
133 if (!result.UsesCacheDependency && !result.IsUpToDate(virtualPath, ensureIsUpToDate)) {
135 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found but is out of date");
137 // Remove it from the cache
138 HttpRuntime.Cache.InternalCache.Remove(key);
140 Debug.Assert(HttpRuntime.Cache.InternalCache.Get(key) == null);
145 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the memory cache");
147 // It's up to date: return it
151 internal override void CacheBuildResult(string cacheKey, BuildResult result,
152 long hashCode, DateTime utcStart) {
154 ICollection virtualDependencies = result.VirtualPathDependencies;
156 Debug.Trace("BuildResultCache", "Adding cache " + cacheKey + " in the memory cache");
158 CacheDependency cacheDependency = null;
160 if (virtualDependencies != null) {
161 cacheDependency = result.VirtualPath.GetCacheDependency(virtualDependencies, utcStart);
163 // If we got a cache dependency, remember that in the BuildResult
164 if (cacheDependency != null)
165 result.UsesCacheDependency = true;
168 // If it should not be cached to memory, leave it alone
169 if (!result.CacheToMemory) {
173 if (BuildResultCompiledType.UsesDelayLoadType(result)) {
174 // If the result is delaying loading of assembly, then don't cache
175 // to avoid having to load the assembly.
179 BuildResultCompiledAssemblyBase compiledResult = result as BuildResultCompiledAssemblyBase;
180 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
182 // Insert a new cache entry using the assembly path as the key
183 string assemblyKey = GetAssemblyCacheKey(compiledResult.ResultAssembly);
184 Assembly a = (Assembly) HttpRuntime.Cache.InternalCache.Get(assemblyKey);
186 Debug.Trace("BuildResultCache", "Adding marker cache entry " + compiledResult.ResultAssembly);
187 // VSWhidbey 500049 - add as NotRemovable to prevent the assembly from being prematurely deleted
188 HttpRuntime.Cache.InternalCache.Insert(assemblyKey, compiledResult.ResultAssembly, null);
191 Debug.Assert(a == compiledResult.ResultAssembly);
194 // Now create a dependency based on that key. This way, by removing that key, we are able to
195 // remove all the pages that live in that assembly from the cache.
196 CacheDependency assemblyCacheDependency = new CacheDependency(0, null, new string[] { assemblyKey });
198 if (cacheDependency != null) {
199 // We can't share the same CacheDependency, since we don't want the UtcStart
200 // behavior for the assembly. Use an Aggregate to put the two together.
201 AggregateCacheDependency tmpDependency = new AggregateCacheDependency();
202 tmpDependency.Add(new CacheDependency[] { cacheDependency, assemblyCacheDependency });
203 cacheDependency = tmpDependency;
206 cacheDependency = assemblyCacheDependency;
210 string key = GetMemoryCacheKey(cacheKey);
212 // Only allow the cache item to expire if the result can be unloaded. Otherwise,
213 // we may as well cache it forever (e.g. for Assemblies and Types).
214 CacheItemPriority cachePriority;
215 if (result.IsUnloadable)
216 cachePriority = CacheItemPriority.Default;
218 cachePriority = CacheItemPriority.NotRemovable;
220 CacheItemRemovedCallback onRemoveCallback = null;
222 // If the appdomain needs to be shut down when the item becomes invalid, register
223 // a callback to do the shutdown.
224 if (result.ShutdownAppDomainOnChange || result is BuildResultCompiledAssemblyBase) {
226 // Create the delegate on demand
227 if (_onRemoveCallback == null)
228 _onRemoveCallback = new CacheItemRemovedCallback(OnCacheItemRemoved);
230 onRemoveCallback = _onRemoveCallback;
233 HttpRuntime.Cache.InternalCache.Insert(key, result, new CacheInsertOptions() {
234 Dependencies = cacheDependency,
235 AbsoluteExpiration = result.MemoryCacheExpiration,
236 SlidingExpiration = result.MemoryCacheSlidingExpiration,
237 Priority = cachePriority,
238 OnRemovedCallback = onRemoveCallback
242 // OnCacheItemRemoved can be invoked with user code on the stack, for example if someone
243 // implements VirtualPathProvider.GetCacheDependency to return a custom CacheDependency.
244 // This callback needs PathDiscovery, Read, and Write permission.
245 [FileIOPermission(SecurityAction.Assert, Unrestricted = true)]
246 private void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) {
248 // Only handle case when the dependency is removed.
249 if (reason == CacheItemRemovedReason.DependencyChanged) {
250 Debug.Trace("BuildResultCache", "OnCacheItemRemoved Key=" + key);
252 // Remove the assembly if a buildresult becomes obsolete
253 if (HostingEnvironment.ShutdownInitiated) {
255 // We still need to mark the affected files and dependencies for later deletion so that we do not build up unused assemblies.
256 RemoveAssemblyAndCleanupDependenciesShuttingDown(value as BuildResultCompiledAssembly);
260 RemoveAssemblyAndCleanupDependencies(value as BuildResultCompiledAssemblyBase);
262 // Shutdown the appdomain if the buildresult requires it.
263 if (((BuildResult)value).ShutdownAppDomainOnChange) {
265 // At this point in code, it is possible that the current thread have acquired the CompilationMutex, and calling
266 // InitiateShutdownWithoutDemand will result in an acquisition of the lock on LockableAppDomainContext.
267 // A deadlock would happen if another thread were starting up, having acquired the lock on LockableAppDomainContext
268 // and going on to perform some compilation thus waiting on the CompilationMutex.
269 // In order to avoid the deadlock, we perform the call to InitiateShutdownWithoutDemand on a separate thread,
270 // so that it is possible for the current thread to continue without blocking or waiting on any lock, and
271 // to release the CompilationMutex later on.
273 ThreadPool.QueueUserWorkItem(new WaitCallback(MemoryBuildResultCache.ShutdownCallBack), "BuildResult change, cache key=" + key);
279 static private void ShutdownCallBack(Object state) {
280 string message = state as string;
281 if (message != null) {
282 HttpRuntime.SetShutdownReason(ApplicationShutdownReason.BuildManagerChange, message);
284 HostingEnvironment.InitiateShutdownWithoutDemand();
287 // Since we are shutting down, we will just create the .delete files to mark the files for deletion,
288 // and not try to get the compilation lock.
289 internal void RemoveAssemblyAndCleanupDependenciesShuttingDown(BuildResultCompiledAssemblyBase compiledResult) {
290 if (compiledResult == null)
293 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
294 string assemblyName = compiledResult.ResultAssembly.GetName().Name;
295 lock (_dependentAssemblies) {
296 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName);
302 internal void RemoveAssemblyAndCleanupDependencies(BuildResultCompiledAssemblyBase compiledResult) {
303 if (compiledResult == null)
306 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
307 RemoveAssemblyAndCleanupDependencies(compiledResult.ResultAssembly.GetName().Name);
311 private void RemoveAssemblyAndCleanupDependencies(string assemblyName) {
312 bool gotLock = false;
315 // Grab the compilation mutex, since we will remove cached build result
316 CompilationLock.GetLock(ref gotLock);
318 // Protect the dependent assemblies table, as it's accessed/modified in the recursion
319 lock (_dependentAssemblies) {
320 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName);
324 // Always release the mutex if we had taken it
326 CompilationLock.ReleaseLock();
328 DiskBuildResultCache.ShutDownAppDomainIfRequired();
332 private void RemoveAssemblyAndCleanupDependenciesNoLock(string assemblyName) {
334 // If we have no cache entry for this assembly, there is nothing to do
335 string cacheKey = GetAssemblyCacheKeyFromName(assemblyName);
336 Assembly assembly = (Assembly)HttpRuntime.Cache.InternalCache.Get(cacheKey);
337 if (assembly == null)
340 // Get the physical path to the assembly
341 String assemblyPath = Util.GetAssemblyCodeBase(assembly);
343 Debug.Trace("BuildResultCache", "removing cacheKey for assembly " + assemblyPath + " because of dependency change");
345 // Remove the cache entry in order to kick out all the pages that are in that batch
346 HttpRuntime.Cache.InternalCache.Remove(cacheKey);
348 // Now call recursively on all the dependent assemblies (VSWhidbey 577593)
349 ICollection dependentAssemblies = _dependentAssemblies[assemblyName] as ICollection;
350 if (dependentAssemblies != null) {
351 foreach (string dependentAssemblyName in dependentAssemblies) {
352 RemoveAssemblyAndCleanupDependenciesNoLock(dependentAssemblyName);
355 // We can now remove this assembly from the hashtable
356 _dependentAssemblies.Remove(cacheKey);
359 // Remove (or rename) the DLL
360 RemoveAssembly(assemblyPath);
363 private static void RemoveAssembly(string path) {
364 var f = new FileInfo(path);
365 DiskBuildResultCache.RemoveAssembly(f);
366 // Delete the associated pdb file as well, since it is possible to
367 // run into a situation where the dependency has changed just
368 // when the cache item is about to get inserted, resulting in
369 // the callback deleting only the dll file and leaving behind the
370 // pdb file. (Dev10 bug 846606)
371 var pdbPath = Path.ChangeExtension(f.FullName, ".pdb");
372 if (File.Exists(pdbPath)) {
373 DiskBuildResultCache.TryDeleteFile(new FileInfo(pdbPath));
377 private static string GetMemoryCacheKey(string cacheKey) {
379 // Prepend something to it to avoid conflicts with other cache users
380 return CacheInternal.PrefixMemoryBuildResult + cacheKey;
384 internal abstract class DiskBuildResultCache: BuildResultCache {
386 protected const string preservationFileExtension = ".compiled";
388 protected string _cacheDir;
390 private static int s_recompilations;
391 private static int s_maxRecompilations = -1;
393 private static bool s_inUseAssemblyWasDeleted;
395 protected const string dotDelete = ".delete";
397 private static int s_shutdownStatus;
398 private const int SHUTDOWN_NEEDED = 1;
399 private const int SHUTDOWN_STARTED = 2;
401 internal DiskBuildResultCache(string cacheDir) {
402 _cacheDir = cacheDir;
404 // Find out how many recompilations we allow before restarting the appdomain
405 if (s_maxRecompilations < 0)
406 s_maxRecompilations = CompilationUtil.GetRecompilationsBeforeAppRestarts();
409 protected void EnsureDiskCacheDirectoryCreated() {
411 // Create the disk cache directory if it's not already there
412 if (!FileUtil.DirectoryExists(_cacheDir)) {
414 Directory.CreateDirectory(_cacheDir);
416 catch (IOException e) {
417 throw new HttpException(SR.GetString(SR.Failed_to_create_temp_dir, HttpRuntime.GetSafePath(_cacheDir)), e);
422 internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) {
424 Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the disk cache");
426 string preservationFile = GetPreservedDataFileName(cacheKey);
428 PreservationFileReader pfr = new PreservationFileReader(this, PrecompilationMode);
430 // Create the BuildResult from the preservation file
431 BuildResult result = pfr.ReadBuildResultFromFile(virtualPath, preservationFile, hashCode, ensureIsUpToDate);
434 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the disk cache");
436 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the disk cache");
441 internal override void CacheBuildResult(string cacheKey, BuildResult result,
442 long hashCode, DateTime utcStart) {
444 // If it should not be cached to disk, leave it alone
445 if (!result.CacheToDisk)
448 // VSWhidbey 564168 don't save to disk if already shutting down, otherwise we might
449 // be persisting assembly that was compiled with obsolete references.
450 // Since we are shutting down and not creating any cache, delete the compiled result
451 // as it will not be used in future.
452 if (HostingEnvironment.ShutdownInitiated) {
453 BuildResultCompiledAssemblyBase compiledResult = result as BuildResultCompiledAssemblyBase;
455 // DevDiv2 880034: check if ResultAssembly is null before calling GetName().
456 // UsesExistingAssembly could be true in updatable compilation scenarios.
457 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly)
458 MarkAssemblyAndRelatedFilesForDeletion(compiledResult.ResultAssembly.GetName().Name);
462 string preservationFile = GetPreservedDataFileName(cacheKey);
463 PreservationFileWriter pfw = new PreservationFileWriter(PrecompilationMode);
465 pfw.SaveBuildResultToFile(preservationFile, result, hashCode);
468 private void MarkAssemblyAndRelatedFilesForDeletion(string assemblyName) {
469 DirectoryInfo directory = new DirectoryInfo(_cacheDir);
470 // Get rid of the prefix "App_web", since related files don't have it
471 string baseName = assemblyName.Substring(BuildManager.WebAssemblyNamePrefix.Length);
472 FileInfo[] files = directory.GetFiles("*" + baseName + ".*");
473 foreach (FileInfo f in files)
474 CreateDotDeleteFile(f);
478 * Return the physical full path to the preservation data file
480 private string GetPreservedDataFileName(string cacheKey) {
482 // Make sure the key doesn't contain any invalid file name chars (VSWhidbey 263142)
483 cacheKey = Util.MakeValidFileName(cacheKey);
485 cacheKey = Path.Combine(_cacheDir, cacheKey);
487 cacheKey = FileUtil.TruncatePathIfNeeded(cacheKey, 9 /*length of ".compiled"*/);
489 // Use a ".compiled" extension for the preservation file
490 return cacheKey + preservationFileExtension;
493 protected virtual bool PrecompilationMode { get { return false; } }
495 internal static bool InUseAssemblyWasDeleted { get { return s_inUseAssemblyWasDeleted; } }
496 internal static void ResetAssemblyDeleted() { s_inUseAssemblyWasDeleted = false; }
499 * Delete an assembly and all its related files. The assembly is typically named
500 * something like ASPNET.jnw_y10n.dll, while related files are simply jnw_y10n.*.
502 internal virtual void RemoveAssemblyAndRelatedFiles(string assemblyName) {
504 Debug.Trace("DiskBuildResultCache", "RemoveAssemblyAndRelatedFiles(" + assemblyName + ")");
506 // If the name doesn't start with the prefix, the cleanup code doesn't apply
507 if (!assemblyName.StartsWith(BuildManager.WebAssemblyNamePrefix, StringComparison.Ordinal)) {
511 // Get rid of the prefix, since related files don't have it
512 string baseName = assemblyName.Substring(BuildManager.WebAssemblyNamePrefix.Length);
514 bool gotLock = false;
516 // Grab the compilation mutex, since we will remove generated assembly
517 CompilationLock.GetLock(ref gotLock);
519 DirectoryInfo directory = new DirectoryInfo(_cacheDir);
521 // Find all the files that contain the base name
522 FileInfo[] files = directory.GetFiles("*" + baseName + ".*");
523 foreach (FileInfo f in files) {
525 if (f.Extension == ".dll") {
526 // Notify existing buildresults that result assembly will be removed.
527 // This is required otherwise new components can be compiled
528 // with obsolete build results whose assembly has been removed.
529 string assemblyKey = GetAssemblyCacheKey(f.FullName);
530 HttpRuntime.Cache.InternalCache.Remove(assemblyKey);
532 // Remove the assembly
535 // Also, remove satellite assemblies that may be associated with it
536 StandardDiskBuildResultCache.RemoveSatelliteAssemblies(assemblyName);
538 else if (f.Extension == dotDelete) {
539 CheckAndRemoveDotDeleteFile(f);
542 // Remove the file, or if not possible, rename it, so it'll get
543 // cleaned up next time by RemoveOldTempFiles()
550 // Always release the mutex if we had taken it
552 CompilationLock.ReleaseLock();
554 DiskBuildResultCache.ShutDownAppDomainIfRequired();
558 internal static void RemoveAssembly(FileInfo f) {
560 // If we are shutting down, just create the .delete file and exit quickly.
561 if (HostingEnvironment.ShutdownInitiated) {
562 CreateDotDeleteFile(f);
566 // VSWhidbey 564168 / Visual Studio QFE 4710
567 // The assembly could still be referenced and needed for compilation in some cases.
568 // Thus, if we cannot delete it, we create an empty .delete file,
569 // so that both will be later removed by RemoveOldTempFiles.
571 // If the file is already marked for deletion, we simply return, so that
572 // we do not double count it in s_recompilations.
573 if (HasDotDeleteFile(f.FullName))
576 if (TryDeleteFile(f))
579 // It had to be renamed, so increment the recompilations count,
580 // and restart the appdomain if it reaches the limit
582 Debug.Trace("DiskBuildResultCache", "RemoveAssembly: " + f.Name + " was renamed");
584 if (++s_recompilations == s_maxRecompilations) {
585 s_shutdownStatus = SHUTDOWN_NEEDED;
588 // Remember the fact that we just invalidated an assembly, which can cause
589 // other BuildResults to become invalid as a side effect (VSWhidbey 269297)
590 s_inUseAssemblyWasDeleted = true;
593 static internal void ShutDownAppDomainIfRequired() {
594 // VSWhidbey 610631 Stress Failure: Worker process throws exceptions while unloading app domain and re-tries over and over
595 // It is possible for a deadlock to happen when locks on ApplicationManager and the CompilationMutex
596 // are acquired in different orders in multiple threads.
597 // Thus, since ShutdownAppDomain acquires a lock on ApplicationManager, we always release the CompilationMutex
598 // before calling ShutdownAppDomain, in case another thread has acquired the lock on ApplicationManager and
599 // is waiting on the CompilationMutex.
602 if (s_shutdownStatus == SHUTDOWN_NEEDED && (Interlocked.Exchange(ref s_shutdownStatus, SHUTDOWN_STARTED) == SHUTDOWN_NEEDED)) {
603 // Perform the actual shutdown on another thread, so that
604 // this thread can proceed and release any compilation mutex it is
605 // holding and not have to block if another thread has acquired a
606 // lock on ApplicationManager.
608 ThreadPool.QueueUserWorkItem(new WaitCallback(DiskBuildResultCache.ShutdownCallBack));
612 static private void ShutdownCallBack(Object state /*not used*/) {
613 HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.MaxRecompilationsReached,
614 "Recompilation limit of " + s_maxRecompilations + " reached");
619 internal static bool TryDeleteFile(string s) {
620 return TryDeleteFile(new FileInfo(s));
623 // Returns true if we are able to delete the file. Otherwise, creates a .delete file and returns false.
624 internal static bool TryDeleteFile(FileInfo f) {
625 if (f.Extension == dotDelete)
626 return CheckAndRemoveDotDeleteFile(f);
630 Debug.Trace("DiskBuildResultCache", "TryDeleteFile removed " + f.Name);
635 CreateDotDeleteFile(f);
639 // Checks if the file is .delete. If it is, check if the associated base file is still around.
640 // If associated base file is around, try to delete it. If success, delete the .delete.
641 // Returns true only if both base file and .delete are removed.
642 internal static bool CheckAndRemoveDotDeleteFile(FileInfo f) {
643 if (f.Extension != dotDelete)
646 string baseName = Path.GetDirectoryName(f.FullName) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(f.FullName);
647 if (FileUtil.FileExists(baseName)) {
649 File.Delete(baseName);
650 Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + baseName);
659 Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + f.Name);
666 internal static bool HasDotDeleteFile(string s) {
667 return File.Exists(s + dotDelete);
670 private static void CreateDotDeleteFile(FileInfo f) {
671 if (f.Extension == dotDelete)
673 string newName = f.FullName + dotDelete;
674 if (!File.Exists(newName)) {
676 (new StreamWriter(newName)).Close();
677 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile succeeded: " + newName);
680 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile failed: " + newName);
681 } // If we fail the .delete probably just got created by another process.
687 internal class StandardDiskBuildResultCache: DiskBuildResultCache {
689 private const string fusionCacheDirectoryName = "assembly";
690 private const string webHashDirectoryName = "hash";
692 private static ArrayList _satelliteDirectories;
694 internal StandardDiskBuildResultCache(string cacheDir)
697 Debug.Assert(cacheDir == HttpRuntime.CodegenDirInternal);
699 EnsureDiskCacheDirectoryCreated();
701 FindSatelliteDirectories();
704 private string GetSpecialFilesCombinedHashFileName() {
705 return BuildManager.WebHashFilePath;
708 internal Tuple<long, long> GetPreservedSpecialFilesCombinedHash() {
709 string fileName = GetSpecialFilesCombinedHashFileName();
710 return GetPreservedSpecialFilesCombinedHash(fileName);
714 * Return the combined hash that was preserved to file. Return 0 if not valid.
716 internal static Tuple<long, long> GetPreservedSpecialFilesCombinedHash(string fileName) {
717 if (!FileUtil.FileExists(fileName)) {
718 return Tuple.Create<long, long>(0, 0);
722 string[] hashTokens = Util.StringFromFile(fileName).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
725 if ((hashTokens.Length == 2) &&
726 Int64.TryParse(hashTokens[0], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out value1) &&
727 Int64.TryParse(hashTokens[1], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out value2)) {
728 return Tuple.Create(value1, value2);
733 // If anything went wrong (file not found, or bad format), return 0
735 return Tuple.Create<long, long>(0, 0);
738 internal void SavePreservedSpecialFilesCombinedHash(Tuple<long, long> hash) {
739 string fileName = GetSpecialFilesCombinedHashFileName();
740 SavePreservedSpecialFilesCombinedHash(fileName, hash);
744 * Preserve the combined hash of the special files to a file.
746 internal static void SavePreservedSpecialFilesCombinedHash(string hashFilePath, Tuple<long, long> hash) {
748 Debug.Assert(hash != null && hash.Item1 != 0 && hash.Item2 != 0, "SavePreservedSpecialFilesCombinedHash: hash0 != 0, hash1 != 0");
750 String hashDirPath = Path.GetDirectoryName(hashFilePath);
752 // Create the hashweb directory if needed
753 if (!FileUtil.DirectoryExists(hashDirPath)) {
754 Directory.CreateDirectory(hashDirPath);
757 using (var writer = new StreamWriter(hashFilePath, false, Encoding.UTF8)) {
758 writer.Write(hash.Item1.ToString("x", CultureInfo.InvariantCulture));
760 writer.Write(hash.Item2.ToString("x", CultureInfo.InvariantCulture));
764 private void FindSatelliteDirectories() {
766 Debug.Assert(_satelliteDirectories == null);
769 // Look for all the subdirectories of the codegen dir that look like
770 // satellite assemblies dirs, and keep track of them
773 string[] subDirs = Directory.GetDirectories(_cacheDir);
775 foreach (string subDir in subDirs) {
776 string subDirName = Path.GetFileNameWithoutExtension(subDir);
778 // Skip the fusion cache, since it's definitely not a culture (VSWhidbey 327716)
779 if (subDirName == fusionCacheDirectoryName)
782 // Skip the "hash" folder
783 if (subDirName == webHashDirectoryName)
786 if (Util.IsCultureName(subDirName)) {
787 if (_satelliteDirectories == null)
788 _satelliteDirectories = new ArrayList();
790 _satelliteDirectories.Add(Path.Combine(_cacheDir, subDir));
795 internal static void RemoveSatelliteAssemblies(string baseAssemblyName) {
797 if (_satelliteDirectories == null)
801 // If any satellite directory contains a satellite assembly that's
802 // for the passed in assembly name, delete it.
805 string satelliteAssemblyName = baseAssemblyName + ".resources";
807 foreach (string satelliteDir in _satelliteDirectories) {
808 string fullAssemblyPath = Path.Combine(satelliteDir, satelliteAssemblyName);
810 // Delete the DLL and PDB
811 Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".dll");
812 Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".pdb");
817 * Delete all temporary files from the codegen directory (e.g. source files, ...)
819 internal void RemoveOldTempFiles() {
820 Debug.Trace("BuildResultCache", "Deleting old temporary files from " + _cacheDir);
822 RemoveCodegenResourceDir();
824 string codegen = _cacheDir + "\\";
826 // Go through all the files in the codegen dir
827 foreach (FileData fileData in FileEnumerator.Create(codegen)) {
830 if (fileData.IsDirectory) continue;
832 // If it has a known extension, skip it
833 string ext = Path.GetExtension(fileData.Name);
834 if (ext == ".dll" || ext == ".pdb" || ext == ".web" || ext == ".ccu" || ext == ".prof" || ext == preservationFileExtension) {
838 // .delete files need to be removed.
839 if (ext != dotDelete) {
840 // Don't delete the temp file if it's named after a dll that's still around
841 // since it could still be useful for debugging.
842 // Note that we can't use GetFileNameWithoutExtension here because
843 // some of the files are named 5hvoxl6v.0.cs, and it would return
844 // 5hvoxl6v.0 instead of just 5hvoxl6v
845 int periodIndex = fileData.Name.LastIndexOf('.');
846 if (periodIndex > 0) {
847 string baseName = fileData.Name.Substring(0, periodIndex);
849 int secondPeriodIndex = baseName.LastIndexOf('.');
850 if (secondPeriodIndex > 0) {
851 baseName = baseName.Substring(0, secondPeriodIndex);
854 // Generated source files uses assemblyname as prefix so we should keep them.
855 if (FileUtil.FileExists(codegen + baseName + ".dll"))
858 // other generated files, such as .cmdline, .err and .out need to add the
859 // WebAssemblyNamePrefix, since they do not use the assembly name as prefix.
860 if (FileUtil.FileExists(codegen + BuildManager.WebAssemblyNamePrefix + baseName + ".dll"))
865 // Additional logic for VSWhidbey 564168 / Visual Studio QFE 4710.
866 // Delete both original .dll and .delete if possible
867 DiskBuildResultCache.CheckAndRemoveDotDeleteFile(new FileInfo(fileData.FullName));
871 Debug.Trace("BuildResultCache", "Deleting old temporary files: " + fileData.FullName);
873 File.Delete(fileData.FullName);
878 private void RemoveCodegenResourceDir() {
879 string path = BuildManager.CodegenResourceDir;
880 Debug.Trace("BuildResultCache", "Deleting codegen temporary resource directory: " + path);
881 if (Directory.Exists(path)){
883 Directory.Delete(path, recursive:true);
890 * Delete all the files in the codegen directory
892 [SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Web.UnsafeNativeMethods.DeleteShadowCache(System.String,System.String)",
893 Justification="Reviewed - we are just trying to clean up the codegen folder as much as possible, so it is ok to ignore any errors.")]
894 internal void RemoveAllCodegenFiles() {
895 Debug.Trace("BuildResultCache", "Deleting all files from " + _cacheDir);
897 RemoveCodegenResourceDir();
899 // Remove everything in the codegen directory, as well as all the subdirectories
900 // used for culture assemblies
902 // Go through all the files in the codegen dir. Delete everything, except
903 // for the fusion cache, which is in the "assembly" subdirectory
904 foreach (FileData fileData in FileEnumerator.Create(_cacheDir)) {
906 // If it's a directories
907 if (fileData.IsDirectory) {
909 // Skip the fusion cache
910 if (fileData.Name == fusionCacheDirectoryName)
913 // Skip the "hash" folder
914 if (fileData.Name == webHashDirectoryName)
917 // Skip the source files generated for the designer (VSWhidbey 138194)
918 if (StringUtil.StringStartsWith(fileData.Name, CodeDirectoryCompiler.sourcesDirectoryPrefix))
922 // If it is a directory, only remove the files inside and not the directory itself
924 DeleteFilesInDirectory(fileData.FullName);
926 catch { } // Ignore all exceptions
931 // VSWhidbey 564168 Do not delete files that cannot be deleted, these files are still
932 // referenced by other appdomains that are in the process of shutting down.
933 // We also do not rename as renaming can cause an assembly not to be found if another
934 // appdomain tries to compile against it.
935 DiskBuildResultCache.TryDeleteFile(fileData.FullName);
940 // Clean up the fusion shadow copy cache
942 AppDomainSetup appDomainSetup = Thread.GetDomain().SetupInformation;
943 UnsafeNativeMethods.DeleteShadowCache(appDomainSetup.CachePath,
944 appDomainSetup.ApplicationName);
947 // Deletes all files in the directory, but leaves the directory there
948 internal void DeleteFilesInDirectory(string path) {
949 foreach (FileData fileData in FileEnumerator.Create(path)) {
950 if (fileData.IsDirectory) {
951 Directory.Delete(fileData.FullName, true /*recursive*/);
954 Util.RemoveOrRenameFile(fileData.FullName);
959 internal abstract class PrecompBaseDiskBuildResultCache: DiskBuildResultCache {
961 // In precompilation, the preservation files go in the bin directory
962 internal PrecompBaseDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
965 // Used when precompiling a site
966 internal class PrecompilerDiskBuildResultCache: PrecompBaseDiskBuildResultCache {
968 internal PrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) {
970 EnsureDiskCacheDirectoryCreated();
974 // Used when precompiling a site using updatable precompilation
975 internal class UpdatablePrecompilerDiskBuildResultCache: PrecompilerDiskBuildResultCache {
977 internal UpdatablePrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
979 internal override void CacheBuildResult(string cacheKey, BuildResult result,
980 long hashCode, DateTime utcStart) {
982 // Don't create preservation files in bin for pages in the updatable model,
983 // because we turn them into a v1 style code behind, which works as a result of
984 // having the aspx file point to the bin class via an inherits attribute.
985 if (result is BuildResultCompiledTemplateType)
988 base.CacheBuildResult(cacheKey, result, hashCode, utcStart);
993 // Used when a site is already precompiled
994 internal class PrecompiledSiteDiskBuildResultCache: PrecompBaseDiskBuildResultCache {
996 internal PrecompiledSiteDiskBuildResultCache(string cacheDir) : base(cacheDir) {}
998 protected override bool PrecompilationMode { get { return true; } }
1000 internal override void CacheBuildResult(string cacheKey, BuildResult result,
1001 long hashCode, DateTime utcStart) {
1003 // Nothing to cache to disk if the app is already precompiled
1006 internal override void RemoveAssemblyAndRelatedFiles(string baseName) {
1007 // Never remove precompiled files (we couldn't anyways since they're