1 //------------------------------------------------------------------------------ 2 // <copyright file="BuildResultCache.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 //------------------------------------------------------------------------------ 6 7 8 9 /********************************* 10 11 BuildResultCache 12 MemoryBuildResultCache 13 DiskBuildResultCache 14 StandardDiskBuildResultCache 15 PrecompBaseDiskBuildResultCache 16 PrecompilerDiskBuildResultCache 17 PrecompiledSiteDiskBuildResultCache 18 19 **********************************/ 20 21 namespace System.Web.Compilation { 22 23 using System; 24 using System.IO; 25 using System.Collections; 26 using System.Diagnostics.CodeAnalysis; 27 using System.Globalization; 28 using System.Text; 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; 35 using System.Web.UI; 36 37 internal abstract class BuildResultCache { GetBuildResult(string cacheKey)38 internal BuildResult GetBuildResult(string cacheKey) { 39 return GetBuildResult(cacheKey, null /*virtualPath*/, 0 /*hashCode*/); 40 } 41 GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate=true)42 internal abstract BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate=true); 43 CacheBuildResult(string cacheKey, BuildResult result, DateTime utcStart)44 internal void CacheBuildResult(string cacheKey, BuildResult result, DateTime utcStart) { 45 CacheBuildResult(cacheKey, result, 0 /*hashCode*/, utcStart); 46 } 47 CacheBuildResult(string cacheKey, BuildResult result, long hashCode, DateTime utcStart)48 internal abstract void CacheBuildResult(string cacheKey, BuildResult result, 49 long hashCode, DateTime utcStart); 50 GetAssemblyCacheKey(string assemblyPath)51 internal static string GetAssemblyCacheKey(string assemblyPath) { 52 string assemblyName = Util.GetAssemblyNameFromFileName(Path.GetFileName(assemblyPath)); 53 return GetAssemblyCacheKeyFromName(assemblyName); 54 } 55 GetAssemblyCacheKey(Assembly assembly)56 internal static string GetAssemblyCacheKey(Assembly assembly) { 57 Debug.Assert(!assembly.GlobalAssemblyCache); 58 return GetAssemblyCacheKeyFromName(assembly.GetName().Name); 59 } 60 GetAssemblyCacheKeyFromName(string assemblyName)61 internal static string GetAssemblyCacheKeyFromName(string assemblyName) { 62 Debug.Assert(StringUtil.StringStartsWith(assemblyName, BuildManager.AssemblyNamePrefix)); 63 return CacheInternal.PrefixAssemblyPath + assemblyName.ToLowerInvariant(); 64 } 65 66 } 67 68 internal class MemoryBuildResultCache: BuildResultCache { 69 70 private CacheItemRemovedCallback _onRemoveCallback; 71 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(); 75 MemoryBuildResultCache()76 internal MemoryBuildResultCache() { 77 78 // Register an AssemblyLoad event 79 AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(OnAssemblyLoad); 80 } 81 82 [PermissionSet(SecurityAction.Assert, Unrestricted = true)] OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)83 private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args) { 84 Assembly a = args.LoadedAssembly; 85 86 // Ignore GAC assemblies 87 if (a.GlobalAssemblyCache) 88 return; 89 90 // Ignore assemblies that don't start with our prefix 91 string name = a.GetName().Name; 92 if (!StringUtil.StringStartsWith(name, BuildManager.AssemblyNamePrefix)) 93 return; 94 95 // Go through all the assemblies it references 96 foreach (AssemblyName assemblyName in a.GetReferencedAssemblies()) { 97 98 // Ignore references that don't start with our prefix 99 if (!StringUtil.StringStartsWith(assemblyName.Name, BuildManager.AssemblyNamePrefix)) 100 continue; 101 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; 109 } 110 111 // Add the assembly that just got loaded as a dependent 112 Debug.Assert(!dependentList.Contains(name)); 113 dependentList.Add(name); 114 } 115 } 116 } 117 GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate)118 internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) { 119 Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the memory cache"); 120 121 string key = GetMemoryCacheKey(cacheKey); 122 BuildResult result = (BuildResult) HttpRuntime.Cache.InternalCache.Get(key); 123 124 // Not found in the cache 125 if (result == null) { 126 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the memory cache"); 127 return null; 128 } 129 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)) { 134 135 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found but is out of date"); 136 137 // Remove it from the cache 138 HttpRuntime.Cache.InternalCache.Remove(key); 139 140 Debug.Assert(HttpRuntime.Cache.InternalCache.Get(key) == null); 141 142 return null; 143 } 144 145 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the memory cache"); 146 147 // It's up to date: return it 148 return result; 149 } 150 CacheBuildResult(string cacheKey, BuildResult result, long hashCode, DateTime utcStart)151 internal override void CacheBuildResult(string cacheKey, BuildResult result, 152 long hashCode, DateTime utcStart) { 153 154 ICollection virtualDependencies = result.VirtualPathDependencies; 155 156 Debug.Trace("BuildResultCache", "Adding cache " + cacheKey + " in the memory cache"); 157 158 CacheDependency cacheDependency = null; 159 160 if (virtualDependencies != null) { 161 cacheDependency = result.VirtualPath.GetCacheDependency(virtualDependencies, utcStart); 162 163 // If we got a cache dependency, remember that in the BuildResult 164 if (cacheDependency != null) 165 result.UsesCacheDependency = true; 166 } 167 168 // If it should not be cached to memory, leave it alone 169 if (!result.CacheToMemory) { 170 return; 171 } 172 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. 176 return; 177 } 178 179 BuildResultCompiledAssemblyBase compiledResult = result as BuildResultCompiledAssemblyBase; 180 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) { 181 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); 185 if (a == null) { 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); 189 } 190 else { 191 Debug.Assert(a == compiledResult.ResultAssembly); 192 } 193 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 }); 197 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; 204 } 205 else { 206 cacheDependency = assemblyCacheDependency; 207 } 208 } 209 210 string key = GetMemoryCacheKey(cacheKey); 211 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; 217 else 218 cachePriority = CacheItemPriority.NotRemovable; 219 220 CacheItemRemovedCallback onRemoveCallback = null; 221 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) { 225 226 // Create the delegate on demand 227 if (_onRemoveCallback == null) 228 _onRemoveCallback = new CacheItemRemovedCallback(OnCacheItemRemoved); 229 230 onRemoveCallback = _onRemoveCallback; 231 } 232 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 239 }); 240 } 241 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)] OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason)246 private void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) { 247 248 // Only handle case when the dependency is removed. 249 if (reason == CacheItemRemovedReason.DependencyChanged) { 250 Debug.Trace("BuildResultCache", "OnCacheItemRemoved Key=" + key); 251 252 // Remove the assembly if a buildresult becomes obsolete 253 if (HostingEnvironment.ShutdownInitiated) { 254 // VSWhidbey 564168 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); 257 } 258 else { 259 260 RemoveAssemblyAndCleanupDependencies(value as BuildResultCompiledAssemblyBase); 261 262 // Shutdown the appdomain if the buildresult requires it. 263 if (((BuildResult)value).ShutdownAppDomainOnChange) { 264 // Dev10 823114 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. 272 273 ThreadPool.QueueUserWorkItem(new WaitCallback(MemoryBuildResultCache.ShutdownCallBack), "BuildResult change, cache key=" + key); 274 } 275 } 276 } 277 } 278 ShutdownCallBack(Object state)279 static private void ShutdownCallBack(Object state) { 280 string message = state as string; 281 if (message != null) { 282 HttpRuntime.SetShutdownReason(ApplicationShutdownReason.BuildManagerChange, message); 283 } 284 HostingEnvironment.InitiateShutdownWithoutDemand(); 285 } 286 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. RemoveAssemblyAndCleanupDependenciesShuttingDown(BuildResultCompiledAssemblyBase compiledResult)289 internal void RemoveAssemblyAndCleanupDependenciesShuttingDown(BuildResultCompiledAssemblyBase compiledResult) { 290 if (compiledResult == null) 291 return; 292 293 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) { 294 string assemblyName = compiledResult.ResultAssembly.GetName().Name; 295 lock (_dependentAssemblies) { 296 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName); 297 } 298 } 299 } 300 301 RemoveAssemblyAndCleanupDependencies(BuildResultCompiledAssemblyBase compiledResult)302 internal void RemoveAssemblyAndCleanupDependencies(BuildResultCompiledAssemblyBase compiledResult) { 303 if (compiledResult == null) 304 return; 305 306 if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) { 307 RemoveAssemblyAndCleanupDependencies(compiledResult.ResultAssembly.GetName().Name); 308 } 309 } 310 RemoveAssemblyAndCleanupDependencies(string assemblyName)311 private void RemoveAssemblyAndCleanupDependencies(string assemblyName) { 312 bool gotLock = false; 313 314 try { 315 // Grab the compilation mutex, since we will remove cached build result 316 CompilationLock.GetLock(ref gotLock); 317 318 // Protect the dependent assemblies table, as it's accessed/modified in the recursion 319 lock (_dependentAssemblies) { 320 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName); 321 } 322 } 323 finally { 324 // Always release the mutex if we had taken it 325 if (gotLock) { 326 CompilationLock.ReleaseLock(); 327 } 328 DiskBuildResultCache.ShutDownAppDomainIfRequired(); 329 } 330 } 331 RemoveAssemblyAndCleanupDependenciesNoLock(string assemblyName)332 private void RemoveAssemblyAndCleanupDependenciesNoLock(string assemblyName) { 333 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) 338 return; 339 340 // Get the physical path to the assembly 341 String assemblyPath = Util.GetAssemblyCodeBase(assembly); 342 343 Debug.Trace("BuildResultCache", "removing cacheKey for assembly " + assemblyPath + " because of dependency change"); 344 345 // Remove the cache entry in order to kick out all the pages that are in that batch 346 HttpRuntime.Cache.InternalCache.Remove(cacheKey); 347 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); 353 } 354 355 // We can now remove this assembly from the hashtable 356 _dependentAssemblies.Remove(cacheKey); 357 } 358 359 // Remove (or rename) the DLL 360 RemoveAssembly(assemblyPath); 361 } 362 RemoveAssembly(string path)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)); 374 } 375 } 376 GetMemoryCacheKey(string cacheKey)377 private static string GetMemoryCacheKey(string cacheKey) { 378 379 // Prepend something to it to avoid conflicts with other cache users 380 return CacheInternal.PrefixMemoryBuildResult + cacheKey; 381 } 382 } 383 384 internal abstract class DiskBuildResultCache: BuildResultCache { 385 386 protected const string preservationFileExtension = ".compiled"; 387 388 protected string _cacheDir; 389 390 private static int s_recompilations; 391 private static int s_maxRecompilations = -1; 392 393 private static bool s_inUseAssemblyWasDeleted; 394 395 protected const string dotDelete = ".delete"; 396 397 private static int s_shutdownStatus; 398 private const int SHUTDOWN_NEEDED = 1; 399 private const int SHUTDOWN_STARTED = 2; 400 DiskBuildResultCache(string cacheDir)401 internal DiskBuildResultCache(string cacheDir) { 402 _cacheDir = cacheDir; 403 404 // Find out how many recompilations we allow before restarting the appdomain 405 if (s_maxRecompilations < 0) 406 s_maxRecompilations = CompilationUtil.GetRecompilationsBeforeAppRestarts(); 407 } 408 EnsureDiskCacheDirectoryCreated()409 protected void EnsureDiskCacheDirectoryCreated() { 410 411 // Create the disk cache directory if it's not already there 412 if (!FileUtil.DirectoryExists(_cacheDir)) { 413 try { 414 Directory.CreateDirectory(_cacheDir); 415 } 416 catch (IOException e) { 417 throw new HttpException(SR.GetString(SR.Failed_to_create_temp_dir, HttpRuntime.GetSafePath(_cacheDir)), e); 418 } 419 } 420 } 421 GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate)422 internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) { 423 424 Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the disk cache"); 425 426 string preservationFile = GetPreservedDataFileName(cacheKey); 427 428 PreservationFileReader pfr = new PreservationFileReader(this, PrecompilationMode); 429 430 // Create the BuildResult from the preservation file 431 BuildResult result = pfr.ReadBuildResultFromFile(virtualPath, preservationFile, hashCode, ensureIsUpToDate); 432 433 if (result != null) 434 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the disk cache"); 435 else 436 Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the disk cache"); 437 438 return result; 439 } 440 CacheBuildResult(string cacheKey, BuildResult result, long hashCode, DateTime utcStart)441 internal override void CacheBuildResult(string cacheKey, BuildResult result, 442 long hashCode, DateTime utcStart) { 443 444 // If it should not be cached to disk, leave it alone 445 if (!result.CacheToDisk) 446 return; 447 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; 454 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); 459 return; 460 } 461 462 string preservationFile = GetPreservedDataFileName(cacheKey); 463 PreservationFileWriter pfw = new PreservationFileWriter(PrecompilationMode); 464 465 pfw.SaveBuildResultToFile(preservationFile, result, hashCode); 466 } 467 MarkAssemblyAndRelatedFilesForDeletion(string assemblyName)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); 475 } 476 477 /* 478 * Return the physical full path to the preservation data file 479 */ GetPreservedDataFileName(string cacheKey)480 private string GetPreservedDataFileName(string cacheKey) { 481 482 // Make sure the key doesn't contain any invalid file name chars (VSWhidbey 263142) 483 cacheKey = Util.MakeValidFileName(cacheKey); 484 485 cacheKey = Path.Combine(_cacheDir, cacheKey); 486 487 cacheKey = FileUtil.TruncatePathIfNeeded(cacheKey, 9 /*length of ".compiled"*/); 488 489 // Use a ".compiled" extension for the preservation file 490 return cacheKey + preservationFileExtension; 491 } 492 493 protected virtual bool PrecompilationMode { get { return false; } } 494 495 internal static bool InUseAssemblyWasDeleted { get { return s_inUseAssemblyWasDeleted; } } ResetAssemblyDeleted()496 internal static void ResetAssemblyDeleted() { s_inUseAssemblyWasDeleted = false; } 497 498 /* 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.*. 501 */ RemoveAssemblyAndRelatedFiles(string assemblyName)502 internal virtual void RemoveAssemblyAndRelatedFiles(string assemblyName) { 503 504 Debug.Trace("DiskBuildResultCache", "RemoveAssemblyAndRelatedFiles(" + assemblyName + ")"); 505 506 // If the name doesn't start with the prefix, the cleanup code doesn't apply 507 if (!assemblyName.StartsWith(BuildManager.WebAssemblyNamePrefix, StringComparison.Ordinal)) { 508 return; 509 } 510 511 // Get rid of the prefix, since related files don't have it 512 string baseName = assemblyName.Substring(BuildManager.WebAssemblyNamePrefix.Length); 513 514 bool gotLock = false; 515 try { 516 // Grab the compilation mutex, since we will remove generated assembly 517 CompilationLock.GetLock(ref gotLock); 518 519 DirectoryInfo directory = new DirectoryInfo(_cacheDir); 520 521 // Find all the files that contain the base name 522 FileInfo[] files = directory.GetFiles("*" + baseName + ".*"); 523 foreach (FileInfo f in files) { 524 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); 531 532 // Remove the assembly 533 RemoveAssembly(f); 534 535 // Also, remove satellite assemblies that may be associated with it 536 StandardDiskBuildResultCache.RemoveSatelliteAssemblies(assemblyName); 537 } 538 else if (f.Extension == dotDelete) { 539 CheckAndRemoveDotDeleteFile(f); 540 } 541 else { 542 // Remove the file, or if not possible, rename it, so it'll get 543 // cleaned up next time by RemoveOldTempFiles() 544 545 TryDeleteFile(f); 546 } 547 } 548 } 549 finally { 550 // Always release the mutex if we had taken it 551 if (gotLock) { 552 CompilationLock.ReleaseLock(); 553 } 554 DiskBuildResultCache.ShutDownAppDomainIfRequired(); 555 } 556 } 557 RemoveAssembly(FileInfo f)558 internal static void RemoveAssembly(FileInfo f) { 559 560 // If we are shutting down, just create the .delete file and exit quickly. 561 if (HostingEnvironment.ShutdownInitiated) { 562 CreateDotDeleteFile(f); 563 return; 564 } 565 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. 570 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)) 574 return; 575 576 if (TryDeleteFile(f)) 577 return; 578 579 // It had to be renamed, so increment the recompilations count, 580 // and restart the appdomain if it reaches the limit 581 582 Debug.Trace("DiskBuildResultCache", "RemoveAssembly: " + f.Name + " was renamed"); 583 584 if (++s_recompilations == s_maxRecompilations) { 585 s_shutdownStatus = SHUTDOWN_NEEDED; 586 } 587 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; 591 } 592 ShutDownAppDomainIfRequired()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. 600 601 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. 607 // (DevDiv 158814) 608 ThreadPool.QueueUserWorkItem(new WaitCallback(DiskBuildResultCache.ShutdownCallBack)); 609 } 610 } 611 ShutdownCallBack(Object state )612 static private void ShutdownCallBack(Object state /*not used*/) { 613 HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.MaxRecompilationsReached, 614 "Recompilation limit of " + s_maxRecompilations + " reached"); 615 } 616 617 618 TryDeleteFile(string s)619 internal static bool TryDeleteFile(string s) { 620 return TryDeleteFile(new FileInfo(s)); 621 } 622 623 // Returns true if we are able to delete the file. Otherwise, creates a .delete file and returns false. TryDeleteFile(FileInfo f)624 internal static bool TryDeleteFile(FileInfo f) { 625 if (f.Extension == dotDelete) 626 return CheckAndRemoveDotDeleteFile(f); 627 628 try { 629 f.Delete(); 630 Debug.Trace("DiskBuildResultCache", "TryDeleteFile removed " + f.Name); 631 return true; 632 } 633 catch { } 634 635 CreateDotDeleteFile(f); 636 return false; 637 } 638 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. CheckAndRemoveDotDeleteFile(FileInfo f)642 internal static bool CheckAndRemoveDotDeleteFile(FileInfo f) { 643 if (f.Extension != dotDelete) 644 return false; 645 646 string baseName = Path.GetDirectoryName(f.FullName) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(f.FullName); 647 if (FileUtil.FileExists(baseName)) { 648 try { 649 File.Delete(baseName); 650 Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + baseName); 651 } 652 catch { 653 return false; 654 } 655 } 656 657 try { 658 f.Delete(); 659 Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + f.Name); 660 } 661 catch { } 662 663 return true; 664 } 665 HasDotDeleteFile(string s)666 internal static bool HasDotDeleteFile(string s) { 667 return File.Exists(s + dotDelete); 668 } 669 CreateDotDeleteFile(FileInfo f)670 private static void CreateDotDeleteFile(FileInfo f) { 671 if (f.Extension == dotDelete) 672 return; 673 string newName = f.FullName + dotDelete; 674 if (!File.Exists(newName)) { 675 try { 676 (new StreamWriter(newName)).Close(); 677 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile succeeded: " + newName); 678 } 679 catch { 680 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile failed: " + newName); 681 } // If we fail the .delete probably just got created by another process. 682 } 683 } 684 685 } 686 687 internal class StandardDiskBuildResultCache: DiskBuildResultCache { 688 689 private const string fusionCacheDirectoryName = "assembly"; 690 private const string webHashDirectoryName = "hash"; 691 692 private static ArrayList _satelliteDirectories; 693 StandardDiskBuildResultCache(string cacheDir)694 internal StandardDiskBuildResultCache(string cacheDir) 695 : base(cacheDir) { 696 697 Debug.Assert(cacheDir == HttpRuntime.CodegenDirInternal); 698 699 EnsureDiskCacheDirectoryCreated(); 700 701 FindSatelliteDirectories(); 702 } 703 GetSpecialFilesCombinedHashFileName()704 private string GetSpecialFilesCombinedHashFileName() { 705 return BuildManager.WebHashFilePath; 706 } 707 GetPreservedSpecialFilesCombinedHash()708 internal Tuple<long, long> GetPreservedSpecialFilesCombinedHash() { 709 string fileName = GetSpecialFilesCombinedHashFileName(); 710 return GetPreservedSpecialFilesCombinedHash(fileName); 711 } 712 713 /* 714 * Return the combined hash that was preserved to file. Return 0 if not valid. 715 */ GetPreservedSpecialFilesCombinedHash(string fileName)716 internal static Tuple<long, long> GetPreservedSpecialFilesCombinedHash(string fileName) { 717 if (!FileUtil.FileExists(fileName)) { 718 return Tuple.Create<long, long>(0, 0); 719 } 720 721 try { 722 string[] hashTokens = Util.StringFromFile(fileName).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 723 724 long value1, value2; 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); 729 } 730 731 } 732 catch { 733 // If anything went wrong (file not found, or bad format), return 0 734 } 735 return Tuple.Create<long, long>(0, 0); 736 } 737 SavePreservedSpecialFilesCombinedHash(Tuple<long, long> hash)738 internal void SavePreservedSpecialFilesCombinedHash(Tuple<long, long> hash) { 739 string fileName = GetSpecialFilesCombinedHashFileName(); 740 SavePreservedSpecialFilesCombinedHash(fileName, hash); 741 } 742 743 /* 744 * Preserve the combined hash of the special files to a file. 745 */ SavePreservedSpecialFilesCombinedHash(string hashFilePath, Tuple<long, long> hash)746 internal static void SavePreservedSpecialFilesCombinedHash(string hashFilePath, Tuple<long, long> hash) { 747 748 Debug.Assert(hash != null && hash.Item1 != 0 && hash.Item2 != 0, "SavePreservedSpecialFilesCombinedHash: hash0 != 0, hash1 != 0"); 749 750 String hashDirPath = Path.GetDirectoryName(hashFilePath); 751 752 // Create the hashweb directory if needed 753 if (!FileUtil.DirectoryExists(hashDirPath)) { 754 Directory.CreateDirectory(hashDirPath); 755 } 756 757 using (var writer = new StreamWriter(hashFilePath, false, Encoding.UTF8)) { 758 writer.Write(hash.Item1.ToString("x", CultureInfo.InvariantCulture)); 759 writer.Write(';'); 760 writer.Write(hash.Item2.ToString("x", CultureInfo.InvariantCulture)); 761 } 762 } 763 FindSatelliteDirectories()764 private void FindSatelliteDirectories() { 765 766 Debug.Assert(_satelliteDirectories == null); 767 768 // 769 // Look for all the subdirectories of the codegen dir that look like 770 // satellite assemblies dirs, and keep track of them 771 // 772 773 string[] subDirs = Directory.GetDirectories(_cacheDir); 774 775 foreach (string subDir in subDirs) { 776 string subDirName = Path.GetFileNameWithoutExtension(subDir); 777 778 // Skip the fusion cache, since it's definitely not a culture (VSWhidbey 327716) 779 if (subDirName == fusionCacheDirectoryName) 780 continue; 781 782 // Skip the "hash" folder 783 if (subDirName == webHashDirectoryName) 784 continue; 785 786 if (Util.IsCultureName(subDirName)) { 787 if (_satelliteDirectories == null) 788 _satelliteDirectories = new ArrayList(); 789 790 _satelliteDirectories.Add(Path.Combine(_cacheDir, subDir)); 791 } 792 } 793 } 794 RemoveSatelliteAssemblies(string baseAssemblyName)795 internal static void RemoveSatelliteAssemblies(string baseAssemblyName) { 796 797 if (_satelliteDirectories == null) 798 return; 799 800 // 801 // If any satellite directory contains a satellite assembly that's 802 // for the passed in assembly name, delete it. 803 // 804 805 string satelliteAssemblyName = baseAssemblyName + ".resources"; 806 807 foreach (string satelliteDir in _satelliteDirectories) { 808 string fullAssemblyPath = Path.Combine(satelliteDir, satelliteAssemblyName); 809 810 // Delete the DLL and PDB 811 Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".dll"); 812 Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".pdb"); 813 } 814 } 815 816 /* 817 * Delete all temporary files from the codegen directory (e.g. source files, ...) 818 */ RemoveOldTempFiles()819 internal void RemoveOldTempFiles() { 820 Debug.Trace("BuildResultCache", "Deleting old temporary files from " + _cacheDir); 821 822 RemoveCodegenResourceDir(); 823 824 string codegen = _cacheDir + "\\"; 825 826 // Go through all the files in the codegen dir 827 foreach (FileData fileData in FileEnumerator.Create(codegen)) { 828 829 // Skip directories 830 if (fileData.IsDirectory) continue; 831 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) { 835 continue; 836 } 837 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); 848 849 int secondPeriodIndex = baseName.LastIndexOf('.'); 850 if (secondPeriodIndex > 0) { 851 baseName = baseName.Substring(0, secondPeriodIndex); 852 } 853 854 // Generated source files uses assemblyname as prefix so we should keep them. 855 if (FileUtil.FileExists(codegen + baseName + ".dll")) 856 continue; 857 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")) 861 continue; 862 } 863 } 864 else { 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)); 868 continue; 869 } 870 871 Debug.Trace("BuildResultCache", "Deleting old temporary files: " + fileData.FullName); 872 try { 873 File.Delete(fileData.FullName); 874 } catch { } 875 } 876 } 877 RemoveCodegenResourceDir()878 private void RemoveCodegenResourceDir() { 879 string path = BuildManager.CodegenResourceDir; 880 Debug.Trace("BuildResultCache", "Deleting codegen temporary resource directory: " + path); 881 if (Directory.Exists(path)){ 882 try { 883 Directory.Delete(path, recursive:true); 884 } 885 catch { } 886 } 887 } 888 889 /* 890 * Delete all the files in the codegen directory 891 */ 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.")] RemoveAllCodegenFiles()894 internal void RemoveAllCodegenFiles() { 895 Debug.Trace("BuildResultCache", "Deleting all files from " + _cacheDir); 896 897 RemoveCodegenResourceDir(); 898 899 // Remove everything in the codegen directory, as well as all the subdirectories 900 // used for culture assemblies 901 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)) { 905 906 // If it's a directories 907 if (fileData.IsDirectory) { 908 909 // Skip the fusion cache 910 if (fileData.Name == fusionCacheDirectoryName) 911 continue; 912 913 // Skip the "hash" folder 914 if (fileData.Name == webHashDirectoryName) 915 continue; 916 917 // Skip the source files generated for the designer (VSWhidbey 138194) 918 if (StringUtil.StringStartsWith(fileData.Name, CodeDirectoryCompiler.sourcesDirectoryPrefix)) 919 continue; 920 921 try { 922 // If it is a directory, only remove the files inside and not the directory itself 923 // VSWhidbey 596757 924 DeleteFilesInDirectory(fileData.FullName); 925 } 926 catch { } // Ignore all exceptions 927 928 continue; 929 } 930 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); 936 937 } 938 939 940 // Clean up the fusion shadow copy cache 941 942 AppDomainSetup appDomainSetup = Thread.GetDomain().SetupInformation; 943 UnsafeNativeMethods.DeleteShadowCache(appDomainSetup.CachePath, 944 appDomainSetup.ApplicationName); 945 } 946 947 // Deletes all files in the directory, but leaves the directory there DeleteFilesInDirectory(string path)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*/); 952 continue; 953 } 954 Util.RemoveOrRenameFile(fileData.FullName); 955 } 956 } 957 } 958 959 internal abstract class PrecompBaseDiskBuildResultCache: DiskBuildResultCache { 960 961 // In precompilation, the preservation files go in the bin directory PrecompBaseDiskBuildResultCache(string cacheDir)962 internal PrecompBaseDiskBuildResultCache(string cacheDir) : base(cacheDir) { } 963 } 964 965 // Used when precompiling a site 966 internal class PrecompilerDiskBuildResultCache: PrecompBaseDiskBuildResultCache { 967 PrecompilerDiskBuildResultCache(string cacheDir)968 internal PrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) { 969 970 EnsureDiskCacheDirectoryCreated(); 971 } 972 } 973 974 // Used when precompiling a site using updatable precompilation 975 internal class UpdatablePrecompilerDiskBuildResultCache: PrecompilerDiskBuildResultCache { 976 UpdatablePrecompilerDiskBuildResultCache(string cacheDir)977 internal UpdatablePrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) { } 978 CacheBuildResult(string cacheKey, BuildResult result, long hashCode, DateTime utcStart)979 internal override void CacheBuildResult(string cacheKey, BuildResult result, 980 long hashCode, DateTime utcStart) { 981 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) 986 return; 987 988 base.CacheBuildResult(cacheKey, result, hashCode, utcStart); 989 } 990 991 } 992 993 // Used when a site is already precompiled 994 internal class PrecompiledSiteDiskBuildResultCache: PrecompBaseDiskBuildResultCache { 995 PrecompiledSiteDiskBuildResultCache(string cacheDir)996 internal PrecompiledSiteDiskBuildResultCache(string cacheDir) : base(cacheDir) {} 997 998 protected override bool PrecompilationMode { get { return true; } } 999 CacheBuildResult(string cacheKey, BuildResult result, long hashCode, DateTime utcStart)1000 internal override void CacheBuildResult(string cacheKey, BuildResult result, 1001 long hashCode, DateTime utcStart) { 1002 1003 // Nothing to cache to disk if the app is already precompiled 1004 } 1005 RemoveAssemblyAndRelatedFiles(string baseName)1006 internal override void RemoveAssemblyAndRelatedFiles(string baseName) { 1007 // Never remove precompiled files (we couldn't anyways since they're 1008 // in the app dir) 1009 } 1010 } 1011 1012 } 1013