Update Reference Sources to .NET Framework 4.6.1
[mono.git] / mcs / class / referencesource / System.Web / Compilation / BuildResultCache.cs
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 {
38     internal BuildResult GetBuildResult(string cacheKey) {
39         return GetBuildResult(cacheKey, null /*virtualPath*/, 0 /*hashCode*/);
40     }
41
42     internal abstract BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate=true);
43
44     internal void CacheBuildResult(string cacheKey, BuildResult result, DateTime utcStart) {
45         CacheBuildResult(cacheKey, result, 0 /*hashCode*/, utcStart);
46     }
47
48     internal abstract void CacheBuildResult(string cacheKey, BuildResult result,
49         long hashCode, DateTime utcStart);
50
51     internal static string GetAssemblyCacheKey(string assemblyPath) {
52         string assemblyName = Util.GetAssemblyNameFromFileName(Path.GetFileName(assemblyPath));
53         return GetAssemblyCacheKeyFromName(assemblyName);
54     }
55
56     internal static string GetAssemblyCacheKey(Assembly assembly) {
57         Debug.Assert(!assembly.GlobalAssemblyCache);
58         return GetAssemblyCacheKeyFromName(assembly.GetName().Name);
59     }
60
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 CacheInternal _cache;
71     private CacheItemRemovedCallback _onRemoveCallback;
72
73     // The keys are simple assembly names
74     // The values are ArrayLists containing the simple names of assemblies that depend on it
75     private Hashtable _dependentAssemblies = new Hashtable();
76
77     internal MemoryBuildResultCache(CacheInternal cache) {
78         _cache = cache;
79
80         // Register an AssemblyLoad event
81         AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(OnAssemblyLoad);
82     }
83
84     [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
85     private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args) {
86         Assembly a = args.LoadedAssembly;
87
88         // Ignore GAC assemblies
89         if (a.GlobalAssemblyCache)
90             return;
91
92         // Ignore assemblies that don't start with our prefix
93         string name = a.GetName().Name;
94         if (!StringUtil.StringStartsWith(name, BuildManager.AssemblyNamePrefix))
95             return;
96
97         // Go through all the assemblies it references
98         foreach (AssemblyName assemblyName in a.GetReferencedAssemblies()) {
99
100             // Ignore references that don't start with our prefix
101             if (!StringUtil.StringStartsWith(assemblyName.Name, BuildManager.AssemblyNamePrefix))
102                 continue;
103
104             lock (_dependentAssemblies) {
105                 // Check whether we already have an ArrayList for this reference
106                 ArrayList dependentList = _dependentAssemblies[assemblyName.Name] as ArrayList;
107                 if (dependentList == null) {
108                     // If not, create one and add it to the hashtable
109                     dependentList = new ArrayList();
110                     _dependentAssemblies[assemblyName.Name] = dependentList;
111                 }
112
113                 // Add the assembly that just got loaded as a dependent
114                 Debug.Assert(!dependentList.Contains(name));
115                 dependentList.Add(name);
116             }
117         }
118     }
119
120     internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) {
121         Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the memory cache");
122
123         string key = GetMemoryCacheKey(cacheKey);
124         BuildResult result = (BuildResult) _cache.Get(key);
125
126         // Not found in the cache
127         if (result == null) {
128             Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the memory cache");
129             return null;
130         }
131
132         // We found it in the cache, but is it up to date.  First, if it uses a CacheDependency,
133         // it must be up to date (this is the default case when using MapPathBasedVirtualPathProvider).
134         // If not, then we need to explicitely check that it's up to date (more expensive)
135         if (!result.UsesCacheDependency && !result.IsUpToDate(virtualPath, ensureIsUpToDate)) {
136
137             Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found but is out of date");
138
139             // Remove it from the cache
140             _cache.Remove(key);
141
142             Debug.Assert(_cache.Get(key) == null);
143
144             return null;
145         }
146
147         Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the memory cache");
148
149         // It's up to date: return it
150         return result;
151     }
152
153     internal override void CacheBuildResult(string cacheKey, BuildResult result,
154         long hashCode, DateTime utcStart) {
155
156         ICollection virtualDependencies = result.VirtualPathDependencies;
157
158         Debug.Trace("BuildResultCache", "Adding cache " + cacheKey + " in the memory cache");
159
160         CacheDependency cacheDependency = null;
161
162         if (virtualDependencies != null) {
163             cacheDependency = result.VirtualPath.GetCacheDependency(virtualDependencies, utcStart);
164
165             // If we got a cache dependency, remember that in the BuildResult
166             if (cacheDependency != null)
167                 result.UsesCacheDependency = true;
168         }
169
170         // If it should not be cached to memory, leave it alone
171         if (!result.CacheToMemory) {
172             return;
173         }
174
175         if (BuildResultCompiledType.UsesDelayLoadType(result)) {
176             // If the result is delaying loading of assembly, then don't cache
177             // to avoid having to load the assembly.
178             return;
179         }
180
181         BuildResultCompiledAssemblyBase compiledResult = result as BuildResultCompiledAssemblyBase;
182         if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
183
184             // Insert a new cache entry using the assembly path as the key
185             string assemblyKey = GetAssemblyCacheKey(compiledResult.ResultAssembly);
186             Assembly a = (Assembly)_cache.Get(assemblyKey);
187             if (a == null) {
188                 Debug.Trace("BuildResultCache", "Adding marker cache entry " + compiledResult.ResultAssembly);
189                 // VSWhidbey 500049 - add as NotRemovable to prevent the assembly from being prematurely deleted
190                 _cache.UtcInsert(assemblyKey, compiledResult.ResultAssembly,
191                                  null,
192                                  Cache.NoAbsoluteExpiration,
193                                  Cache.NoSlidingExpiration,
194                                  CacheItemPriority.NotRemovable,
195                                  null);
196             }
197             else {
198                 Debug.Assert(a == compiledResult.ResultAssembly);
199             }
200
201             // Now create a dependency based on that key. This way, by removing that key, we are able to
202             // remove all the pages that live in that assembly from the cache.
203             CacheDependency assemblyCacheDependency = new CacheDependency(0, null, new string[] { assemblyKey });
204
205             if (cacheDependency != null) {
206                 // We can't share the same CacheDependency, since we don't want the UtcStart
207                 // behavior for the assembly.  Use an Aggregate to put the two together.
208                 AggregateCacheDependency tmpDependency = new AggregateCacheDependency();
209                 tmpDependency.Add(new CacheDependency[] { cacheDependency, assemblyCacheDependency });
210                 cacheDependency = tmpDependency;
211             }
212             else {
213                 cacheDependency = assemblyCacheDependency;
214             }
215         }
216
217         string key = GetMemoryCacheKey(cacheKey);
218
219         // Only allow the cache item to expire if the result can be unloaded.  Otherwise,
220         // we may as well cache it forever (e.g. for Assemblies and Types).
221         CacheItemPriority cachePriority;
222         if (result.IsUnloadable)
223             cachePriority = CacheItemPriority.Default;
224         else
225             cachePriority = CacheItemPriority.NotRemovable;
226
227         CacheItemRemovedCallback onRemoveCallback = null;
228
229         // If the appdomain needs to be shut down when the item becomes invalid, register
230         // a callback to do the shutdown.
231         if (result.ShutdownAppDomainOnChange || result is BuildResultCompiledAssemblyBase) {
232
233             // Create the delegate on demand
234             if (_onRemoveCallback == null)
235                 _onRemoveCallback = new CacheItemRemovedCallback(OnCacheItemRemoved);
236
237             onRemoveCallback = _onRemoveCallback;
238         }
239
240         _cache.UtcInsert(key, result, cacheDependency,
241             result.MemoryCacheExpiration,
242             result.MemoryCacheSlidingExpiration,
243             cachePriority,
244             onRemoveCallback);
245     }
246
247     // OnCacheItemRemoved can be invoked with user code on the stack, for example if someone
248     // implements VirtualPathProvider.GetCacheDependency to return a custom CacheDependency.
249     // This callback needs PathDiscovery, Read, and Write permission.
250     [FileIOPermission(SecurityAction.Assert, Unrestricted = true)]
251     private void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason) {
252
253         // Only handle case when the dependency is removed.
254         if (reason == CacheItemRemovedReason.DependencyChanged) {
255             Debug.Trace("BuildResultCache", "OnCacheItemRemoved Key=" + key);
256
257             // Remove the assembly if a buildresult becomes obsolete
258             if (HostingEnvironment.ShutdownInitiated) {
259                 // VSWhidbey 564168
260                 // We still need to mark the affected files and dependencies for later deletion so that we do not build up unused assemblies.
261                 RemoveAssemblyAndCleanupDependenciesShuttingDown(value as BuildResultCompiledAssembly);
262             }
263             else {
264
265                 RemoveAssemblyAndCleanupDependencies(value as BuildResultCompiledAssemblyBase);
266
267                 // Shutdown the appdomain if the buildresult requires it.
268                 if (((BuildResult)value).ShutdownAppDomainOnChange) {
269                     // Dev10 823114
270                     // At this point in code, it is possible that the current thread have acquired the CompilationMutex, and calling
271                     // InitiateShutdownWithoutDemand will result in an acquisition of the lock on LockableAppDomainContext. 
272                     // A deadlock would happen if another thread were starting up, having acquired the lock on LockableAppDomainContext 
273                     // and going on to perform some compilation thus waiting on the CompilationMutex.
274                     // In order to avoid the deadlock, we perform the call to InitiateShutdownWithoutDemand on a separate thread,
275                     // so that it is possible for the current thread to continue without blocking or waiting on any lock, and 
276                     // to release the CompilationMutex later on.
277
278                     ThreadPool.QueueUserWorkItem(new WaitCallback(MemoryBuildResultCache.ShutdownCallBack), "BuildResult change, cache key=" + key);
279                 }
280             }
281         }
282     }
283
284     static private void ShutdownCallBack(Object state) {
285         string message = state as string;
286         if (message != null) {
287             HttpRuntime.SetShutdownReason(ApplicationShutdownReason.BuildManagerChange, message);
288         }
289         HostingEnvironment.InitiateShutdownWithoutDemand();
290     }
291
292     // Since we are shutting down, we will just create the .delete files to mark the files for deletion,
293     // and not try to get the compilation lock.
294     internal void RemoveAssemblyAndCleanupDependenciesShuttingDown(BuildResultCompiledAssemblyBase compiledResult) {
295         if (compiledResult == null)
296             return;
297
298         if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
299             string assemblyName = compiledResult.ResultAssembly.GetName().Name;
300             lock (_dependentAssemblies) {
301                 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName);
302             }
303         }
304     }
305
306
307     internal void RemoveAssemblyAndCleanupDependencies(BuildResultCompiledAssemblyBase compiledResult) {
308         if (compiledResult == null)
309             return;
310
311         if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly) {
312             RemoveAssemblyAndCleanupDependencies(compiledResult.ResultAssembly.GetName().Name);
313         }
314     }
315
316     private void RemoveAssemblyAndCleanupDependencies(string assemblyName) {
317         bool gotLock = false;
318
319         try {
320             // Grab the compilation mutex, since we will remove cached build result
321             CompilationLock.GetLock(ref gotLock);
322
323             // Protect the dependent assemblies table, as it's accessed/modified in the recursion
324             lock (_dependentAssemblies) {
325                 RemoveAssemblyAndCleanupDependenciesNoLock(assemblyName);
326             }
327         }
328         finally {
329             // Always release the mutex if we had taken it
330             if (gotLock) {
331                 CompilationLock.ReleaseLock();
332             }
333             DiskBuildResultCache.ShutDownAppDomainIfRequired();
334         }
335     }
336
337     private void RemoveAssemblyAndCleanupDependenciesNoLock(string assemblyName) {
338
339         // If we have no cache entry for this assembly, there is nothing to do
340         string cacheKey = GetAssemblyCacheKeyFromName(assemblyName);
341         Assembly assembly = (Assembly)_cache[cacheKey];
342         if (assembly == null)
343             return;
344
345         // Get the physical path to the assembly
346         String assemblyPath = Util.GetAssemblyCodeBase(assembly);
347
348         Debug.Trace("BuildResultCache", "removing cacheKey for assembly " + assemblyPath + " because of dependency change");
349
350         // Remove the cache entry in order to kick out all the pages that are in that batch
351         _cache.Remove(cacheKey);
352
353         // Now call recursively on all the dependent assemblies (VSWhidbey 577593)
354         ICollection dependentAssemblies = _dependentAssemblies[assemblyName] as ICollection;
355         if (dependentAssemblies != null) {
356             foreach (string dependentAssemblyName in dependentAssemblies) {
357                 RemoveAssemblyAndCleanupDependenciesNoLock(dependentAssemblyName);
358             }
359
360             // We can now remove this assembly from the hashtable
361             _dependentAssemblies.Remove(cacheKey);
362         }
363
364         // Remove (or rename) the DLL
365         RemoveAssembly(assemblyPath);
366     }
367
368     private static void RemoveAssembly(string path) {
369         var f = new FileInfo(path);
370         DiskBuildResultCache.RemoveAssembly(f);
371         // Delete the associated pdb file as well, since it is possible to
372         // run into a situation where the dependency has changed just
373         // when the cache item is about to get inserted, resulting in 
374         // the callback deleting only the dll file and leaving behind the 
375         // pdb file. (Dev10 bug 846606)
376         var pdbPath = Path.ChangeExtension(f.FullName, ".pdb");
377         if (File.Exists(pdbPath)) {
378             DiskBuildResultCache.TryDeleteFile(new FileInfo(pdbPath));
379         }
380     }
381
382     private static string GetMemoryCacheKey(string cacheKey) {
383
384         // Prepend something to it to avoid conflicts with other cache users
385         return CacheInternal.PrefixMemoryBuildResult + cacheKey;
386     }
387 }
388
389 internal abstract class DiskBuildResultCache: BuildResultCache {
390
391     protected const string preservationFileExtension = ".compiled";
392
393     protected string _cacheDir;
394
395     private static int s_recompilations;
396     private static int s_maxRecompilations = -1;
397
398     private static bool s_inUseAssemblyWasDeleted;
399
400     protected const string dotDelete = ".delete";
401
402     private static int s_shutdownStatus;
403     private const int SHUTDOWN_NEEDED = 1;
404     private const int SHUTDOWN_STARTED = 2;
405
406     internal DiskBuildResultCache(string cacheDir) {
407         _cacheDir = cacheDir;
408
409         // Find out how many recompilations we allow before restarting the appdomain
410         if (s_maxRecompilations < 0)
411             s_maxRecompilations = CompilationUtil.GetRecompilationsBeforeAppRestarts();
412     }
413
414     protected void EnsureDiskCacheDirectoryCreated() {
415
416         // Create the disk cache directory if it's not already there
417         if (!FileUtil.DirectoryExists(_cacheDir)) {
418             try {
419                 Directory.CreateDirectory(_cacheDir);
420             }
421             catch (IOException e) {
422                 throw new HttpException(SR.GetString(SR.Failed_to_create_temp_dir, HttpRuntime.GetSafePath(_cacheDir)), e);
423             }
424         }
425     }
426
427     internal override BuildResult GetBuildResult(string cacheKey, VirtualPath virtualPath, long hashCode, bool ensureIsUpToDate) {
428
429         Debug.Trace("BuildResultCache", "Looking for '" + cacheKey + "' in the disk cache");
430         
431         string preservationFile = GetPreservedDataFileName(cacheKey);
432
433         PreservationFileReader pfr = new PreservationFileReader(this, PrecompilationMode);
434
435         // Create the BuildResult from the preservation file
436         BuildResult result = pfr.ReadBuildResultFromFile(virtualPath, preservationFile, hashCode, ensureIsUpToDate);
437
438         if (result != null)
439             Debug.Trace("BuildResultCache", "'" + cacheKey + "' was found in the disk cache");
440         else
441             Debug.Trace("BuildResultCache", "'" + cacheKey + "' was not found in the disk cache");
442
443         return result;
444     }
445
446     internal override void CacheBuildResult(string cacheKey, BuildResult result,
447         long hashCode, DateTime utcStart) {
448
449         // If it should not be cached to disk, leave it alone
450         if (!result.CacheToDisk)
451             return;
452
453         // VSWhidbey 564168 don't save to disk if already shutting down, otherwise we might
454         // be persisting assembly that was compiled with obsolete references.
455         // Since we are shutting down and not creating any cache, delete the compiled result 
456         // as it will not be used in future.
457         if (HostingEnvironment.ShutdownInitiated) {
458             BuildResultCompiledAssemblyBase compiledResult = result as BuildResultCompiledAssemblyBase;
459
460             // DevDiv2 880034: check if ResultAssembly is null before calling GetName().
461             // UsesExistingAssembly could be true in updatable compilation scenarios.
462             if (compiledResult != null && compiledResult.ResultAssembly != null && !compiledResult.UsesExistingAssembly)
463                 MarkAssemblyAndRelatedFilesForDeletion(compiledResult.ResultAssembly.GetName().Name);
464             return;
465         }
466
467         string preservationFile = GetPreservedDataFileName(cacheKey);
468         PreservationFileWriter pfw = new PreservationFileWriter(PrecompilationMode);
469
470         pfw.SaveBuildResultToFile(preservationFile, result, hashCode);
471     }
472
473     private void MarkAssemblyAndRelatedFilesForDeletion(string assemblyName) {
474         DirectoryInfo directory = new DirectoryInfo(_cacheDir);
475         // Get rid of the prefix "App_web", since related files don't have it
476         string baseName = assemblyName.Substring(BuildManager.WebAssemblyNamePrefix.Length);
477         FileInfo[] files = directory.GetFiles("*" + baseName + ".*");
478         foreach (FileInfo f in files)
479             CreateDotDeleteFile(f);
480     }
481
482     /*
483      * Return the physical full path to the preservation data file
484      */
485     private string GetPreservedDataFileName(string cacheKey) {
486
487         // Make sure the key doesn't contain any invalid file name chars (VSWhidbey 263142)
488         cacheKey = Util.MakeValidFileName(cacheKey);
489         
490         cacheKey = Path.Combine(_cacheDir, cacheKey);
491
492         cacheKey = FileUtil.TruncatePathIfNeeded(cacheKey, 9 /*length of ".compiled"*/);
493
494         // Use a ".compiled" extension for the preservation file
495         return cacheKey + preservationFileExtension;
496     }
497
498     protected virtual bool PrecompilationMode { get { return false; } }
499
500     internal static bool InUseAssemblyWasDeleted { get { return s_inUseAssemblyWasDeleted; } }
501     internal static void ResetAssemblyDeleted() { s_inUseAssemblyWasDeleted = false; }
502
503     /*
504      * Delete an assembly and all its related files.  The assembly is typically named
505      * something like ASPNET.jnw_y10n.dll, while related files are simply jnw_y10n.*.
506      */
507     internal virtual void RemoveAssemblyAndRelatedFiles(string assemblyName) {
508
509         Debug.Trace("DiskBuildResultCache", "RemoveAssemblyAndRelatedFiles(" + assemblyName + ")");
510
511         // If the name doesn't start with the prefix, the cleanup code doesn't apply
512         if (!assemblyName.StartsWith(BuildManager.WebAssemblyNamePrefix, StringComparison.Ordinal)) {
513             return;
514         }
515
516         // Get rid of the prefix, since related files don't have it
517         string baseName = assemblyName.Substring(BuildManager.WebAssemblyNamePrefix.Length);
518
519         bool gotLock = false;
520         try {
521             // Grab the compilation mutex, since we will remove generated assembly
522             CompilationLock.GetLock(ref gotLock);
523
524             DirectoryInfo directory = new DirectoryInfo(_cacheDir);
525
526             // Find all the files that contain the base name
527             FileInfo[] files = directory.GetFiles("*" + baseName + ".*");
528             foreach (FileInfo f in files) {
529         
530                 if (f.Extension == ".dll") {
531                     // Notify existing buildresults that result assembly will be removed.
532                     // This is required otherwise new components can be compiled
533                     // with obsolete build results whose assembly has been removed.
534                     string assemblyKey = GetAssemblyCacheKey(f.FullName);
535                     HttpRuntime.CacheInternal.Remove(assemblyKey);
536
537                     // Remove the assembly
538                     RemoveAssembly(f);
539
540                     // Also, remove satellite assemblies that may be associated with it
541                     StandardDiskBuildResultCache.RemoveSatelliteAssemblies(assemblyName);
542                 }
543                 else if (f.Extension == dotDelete) {
544                     CheckAndRemoveDotDeleteFile(f);
545                 }
546                 else {
547                     // Remove the file, or if not possible, rename it, so it'll get
548                     // cleaned up next time by RemoveOldTempFiles()
549
550                     TryDeleteFile(f);
551                 }
552             }
553         }
554         finally {
555             // Always release the mutex if we had taken it
556             if (gotLock) {
557                 CompilationLock.ReleaseLock();
558             }
559             DiskBuildResultCache.ShutDownAppDomainIfRequired();
560         }
561     }
562
563     internal static void RemoveAssembly(FileInfo f) {
564
565         // If we are shutting down, just create the .delete file and exit quickly.
566         if (HostingEnvironment.ShutdownInitiated) {
567             CreateDotDeleteFile(f);
568             return;
569         }
570
571         // VSWhidbey 564168 / Visual Studio QFE 4710
572         // The assembly could still be referenced and needed for compilation in some cases.
573         // Thus, if we cannot delete it, we create an empty .delete file,
574         // so that both will be later removed by RemoveOldTempFiles.
575
576         // If the file is already marked for deletion, we simply return, so that
577         // we do not double count it in s_recompilations.
578         if (HasDotDeleteFile(f.FullName))
579             return;
580
581         if (TryDeleteFile(f))
582             return;
583             
584         // It had to be renamed, so increment the recompilations count,
585         // and restart the appdomain if it reaches the limit
586
587         Debug.Trace("DiskBuildResultCache", "RemoveAssembly: " + f.Name + " was renamed");
588
589         if (++s_recompilations == s_maxRecompilations) {
590             s_shutdownStatus = SHUTDOWN_NEEDED;
591         }
592
593         // Remember the fact that we just invalidated an assembly, which can cause
594         // other BuildResults to become invalid as a side effect (VSWhidbey 269297)
595         s_inUseAssemblyWasDeleted = true;
596     }
597
598     static internal void ShutDownAppDomainIfRequired() {
599         // VSWhidbey 610631 Stress Failure: Worker process throws exceptions while unloading app domain and re-tries over and over
600         // It is possible for a deadlock to happen when locks on ApplicationManager and the CompilationMutex
601         // are acquired in different orders in multiple threads.
602         // Thus, since ShutdownAppDomain acquires a lock on ApplicationManager, we always release the CompilationMutex
603         // before calling ShutdownAppDomain, in case another thread has acquired the lock on ApplicationManager and
604         // is waiting on the CompilationMutex.
605
606
607         if (s_shutdownStatus == SHUTDOWN_NEEDED && (Interlocked.Exchange(ref s_shutdownStatus, SHUTDOWN_STARTED) == SHUTDOWN_NEEDED)) {
608             // Perform the actual shutdown on another thread, so that 
609             // this thread can proceed and release any compilation mutex it is
610             // holding and not have to block if another thread has acquired a 
611             // lock on ApplicationManager.
612             // (DevDiv 158814)
613             ThreadPool.QueueUserWorkItem(new WaitCallback(DiskBuildResultCache.ShutdownCallBack));
614         }
615     }
616
617     static private void ShutdownCallBack(Object state /*not used*/) {
618         HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.MaxRecompilationsReached,
619             "Recompilation limit of " + s_maxRecompilations + " reached");
620     }
621
622
623
624     internal static bool TryDeleteFile(string s) {
625         return TryDeleteFile(new FileInfo(s));
626     }
627
628     // Returns true if we are able to delete the file. Otherwise, creates a .delete file and returns false.
629     internal static bool TryDeleteFile(FileInfo f) {
630         if (f.Extension == dotDelete)
631             return CheckAndRemoveDotDeleteFile(f);
632
633         try {
634             f.Delete();
635             Debug.Trace("DiskBuildResultCache", "TryDeleteFile removed " + f.Name);
636             return true;
637         } 
638         catch { }
639
640         CreateDotDeleteFile(f);
641         return false;
642     }
643
644     // Checks if the file is .delete. If it is, check if the associated base file is still around.
645     // If associated base file is around, try to delete it. If success, delete the .delete.
646     // Returns true only if both base file and .delete are removed.
647     internal static bool CheckAndRemoveDotDeleteFile(FileInfo f) {
648         if (f.Extension != dotDelete)
649             return false;
650
651         string baseName = Path.GetDirectoryName(f.FullName) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(f.FullName);
652         if (FileUtil.FileExists(baseName)) {
653             try {
654                 File.Delete(baseName);
655                 Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + baseName);
656             } 
657             catch {
658                 return false;
659             }            
660         }
661
662         try {
663             f.Delete();
664             Debug.Trace("DiskBuildResultCache", "CheckAndRemoveDotDeleteFile deleted " + f.Name);
665         } 
666         catch { }
667         
668         return true;
669     }
670
671     internal static bool HasDotDeleteFile(string s) {
672         return File.Exists(s + dotDelete);
673     }
674
675     private static void CreateDotDeleteFile(FileInfo f) {
676         if (f.Extension == dotDelete)
677             return;
678         string newName = f.FullName + dotDelete;
679         if (!File.Exists(newName)) {
680             try {
681                 (new StreamWriter(newName)).Close();
682                 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile succeeded: " + newName);
683             }
684             catch {
685                 Debug.Trace("DiskBuildResultCache", "CreateDotDeleteFile failed: " + newName);
686             } // If we fail the .delete probably just got created by another process.
687         }
688     }
689
690 }
691
692 internal class StandardDiskBuildResultCache: DiskBuildResultCache {
693
694     private const string fusionCacheDirectoryName = "assembly";
695     private const string webHashDirectoryName = "hash";
696
697     private static ArrayList _satelliteDirectories;
698
699     internal StandardDiskBuildResultCache(string cacheDir)
700         : base(cacheDir) {
701
702         Debug.Assert(cacheDir == HttpRuntime.CodegenDirInternal);
703
704         EnsureDiskCacheDirectoryCreated();
705
706         FindSatelliteDirectories();
707     }
708
709     private string GetSpecialFilesCombinedHashFileName() {
710         return BuildManager.WebHashFilePath;
711     }
712
713     internal Tuple<long, long> GetPreservedSpecialFilesCombinedHash() {
714         string fileName = GetSpecialFilesCombinedHashFileName();
715         return GetPreservedSpecialFilesCombinedHash(fileName);
716     }
717
718     /*
719      * Return the combined hash that was preserved to file.  Return 0 if not valid.
720      */
721     internal static Tuple<long, long> GetPreservedSpecialFilesCombinedHash(string fileName) {
722         if (!FileUtil.FileExists(fileName)) {
723             return Tuple.Create<long, long>(0, 0);
724         }
725
726         try {
727             string[] hashTokens = Util.StringFromFile(fileName).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
728
729             long value1, value2;
730             if ((hashTokens.Length == 2) &&
731                     Int64.TryParse(hashTokens[0], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out value1) &&
732                     Int64.TryParse(hashTokens[1], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out value2)) {
733                 return Tuple.Create(value1, value2);
734             }
735             
736         }
737         catch {
738             // If anything went wrong (file not found, or bad format), return 0
739         }
740         return Tuple.Create<long, long>(0, 0);
741     }
742
743     internal void SavePreservedSpecialFilesCombinedHash(Tuple<long, long> hash) {
744         string fileName = GetSpecialFilesCombinedHashFileName();
745         SavePreservedSpecialFilesCombinedHash(fileName, hash);
746     }
747
748     /*
749      * Preserve the combined hash of the special files to a file.
750      */
751     internal static void SavePreservedSpecialFilesCombinedHash(string hashFilePath, Tuple<long, long> hash) {
752
753         Debug.Assert(hash != null && hash.Item1 != 0 && hash.Item2 != 0, "SavePreservedSpecialFilesCombinedHash: hash0 != 0, hash1 != 0");
754         
755         String hashDirPath = Path.GetDirectoryName(hashFilePath);
756
757         // Create the hashweb directory if needed
758         if (!FileUtil.DirectoryExists(hashDirPath)) {
759             Directory.CreateDirectory(hashDirPath);
760         }
761
762         using (var writer = new StreamWriter(hashFilePath, false, Encoding.UTF8)) {
763             writer.Write(hash.Item1.ToString("x", CultureInfo.InvariantCulture));
764             writer.Write(';');
765             writer.Write(hash.Item2.ToString("x", CultureInfo.InvariantCulture));
766         }
767     }
768
769     private void FindSatelliteDirectories() {
770
771         Debug.Assert(_satelliteDirectories == null);
772
773         //
774         // Look for all the subdirectories of the codegen dir that look like
775         // satellite assemblies dirs, and keep track of them
776         //
777
778         string[] subDirs = Directory.GetDirectories(_cacheDir);
779
780         foreach (string subDir in subDirs) {
781             string subDirName = Path.GetFileNameWithoutExtension(subDir);
782
783             // Skip the fusion cache, since it's definitely not a culture (VSWhidbey 327716)
784             if (subDirName == fusionCacheDirectoryName)
785                 continue;
786
787             // Skip the "hash" folder
788             if (subDirName == webHashDirectoryName)
789                 continue;
790
791             if (Util.IsCultureName(subDirName)) {
792                 if (_satelliteDirectories == null)
793                     _satelliteDirectories = new ArrayList();
794
795                 _satelliteDirectories.Add(Path.Combine(_cacheDir, subDir));
796             }
797         }
798     }
799
800     internal static void RemoveSatelliteAssemblies(string baseAssemblyName) {
801
802         if (_satelliteDirectories == null)
803             return;
804
805         //
806         // If any satellite directory contains a satellite assembly that's
807         // for the passed in assembly name, delete it.
808         //
809
810         string satelliteAssemblyName = baseAssemblyName + ".resources";
811
812         foreach (string satelliteDir in _satelliteDirectories) {
813             string fullAssemblyPath = Path.Combine(satelliteDir, satelliteAssemblyName);
814
815             // Delete the DLL and PDB
816             Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".dll");
817             Util.DeleteFileIfExistsNoException(fullAssemblyPath + ".pdb");
818         }
819     }
820
821     /*
822      * Delete all temporary files from the codegen directory (e.g. source files, ...)
823      */
824     internal void RemoveOldTempFiles() {
825         Debug.Trace("BuildResultCache", "Deleting old temporary files from " + _cacheDir);
826
827         RemoveCodegenResourceDir();
828
829         string codegen = _cacheDir + "\\";
830
831         // Go through all the files in the codegen dir
832         foreach (FileData fileData in FileEnumerator.Create(codegen)) {
833
834             // Skip directories
835             if (fileData.IsDirectory) continue;
836
837             // If it has a known extension, skip it
838             string ext = Path.GetExtension(fileData.Name);
839             if (ext == ".dll" || ext == ".pdb" || ext == ".web" || ext == ".ccu" || ext == ".prof" || ext == preservationFileExtension) {
840                 continue;
841             }
842
843             // .delete files need to be removed.
844             if (ext != dotDelete) {
845                 // Don't delete the temp file if it's named after a dll that's still around
846                 // since it could still be useful for debugging.
847                 // Note that we can't use GetFileNameWithoutExtension here because
848                 // some of the files are named 5hvoxl6v.0.cs, and it would return
849                 // 5hvoxl6v.0 instead of just 5hvoxl6v
850                 int periodIndex = fileData.Name.LastIndexOf('.');
851                 if (periodIndex > 0) {
852                     string baseName = fileData.Name.Substring(0, periodIndex);
853
854                     int secondPeriodIndex = baseName.LastIndexOf('.');
855                     if (secondPeriodIndex > 0) {
856                         baseName = baseName.Substring(0, secondPeriodIndex);
857                     }
858
859                     // Generated source files uses assemblyname as prefix so we should keep them.
860                     if (FileUtil.FileExists(codegen + baseName + ".dll"))
861                         continue;
862
863                     // other generated files, such as .cmdline, .err and .out need to add the 
864                     // WebAssemblyNamePrefix, since they do not use the assembly name as prefix.
865                     if (FileUtil.FileExists(codegen + BuildManager.WebAssemblyNamePrefix + baseName + ".dll"))
866                         continue;
867                 }
868             }
869             else {
870                 // Additional logic for VSWhidbey 564168 / Visual Studio QFE 4710.
871                 // Delete both original .dll and .delete if possible
872                 DiskBuildResultCache.CheckAndRemoveDotDeleteFile(new FileInfo(fileData.FullName));
873                 continue;
874             }
875
876             Debug.Trace("BuildResultCache", "Deleting old temporary files: " + fileData.FullName);
877             try {
878                 File.Delete(fileData.FullName);
879             } catch { }
880         }
881     }
882
883     private void RemoveCodegenResourceDir() {
884         string path = BuildManager.CodegenResourceDir;
885         Debug.Trace("BuildResultCache", "Deleting codegen temporary resource directory: " + path);
886         if (Directory.Exists(path)){
887             try {
888                 Directory.Delete(path, recursive:true);
889             } 
890             catch { }
891         }
892     }
893
894     /*
895      * Delete all the files in the codegen directory
896      */
897     [SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Web.UnsafeNativeMethods.DeleteShadowCache(System.String,System.String)", 
898         Justification="Reviewed - we are just trying to clean up the codegen folder as much as possible, so it is ok to ignore any errors.")]
899     internal void RemoveAllCodegenFiles() {
900         Debug.Trace("BuildResultCache", "Deleting all files from " + _cacheDir);
901
902         RemoveCodegenResourceDir();
903
904         // Remove everything in the codegen directory, as well as all the subdirectories
905         // used for culture assemblies
906
907         // Go through all the files in the codegen dir.  Delete everything, except
908         // for the fusion cache, which is in the "assembly" subdirectory
909         foreach (FileData fileData in FileEnumerator.Create(_cacheDir)) {
910
911             // If it's a directories
912             if (fileData.IsDirectory) {
913
914                 // Skip the fusion cache
915                 if (fileData.Name == fusionCacheDirectoryName)
916                     continue;
917
918                 // Skip the "hash" folder
919                 if (fileData.Name == webHashDirectoryName)
920                     continue;
921
922                 // Skip the source files generated for the designer (VSWhidbey 138194)
923                 if (StringUtil.StringStartsWith(fileData.Name, CodeDirectoryCompiler.sourcesDirectoryPrefix))
924                     continue;
925
926                 try {
927                     // If it is a directory, only remove the files inside and not the directory itself
928                     // VSWhidbey 596757
929                     DeleteFilesInDirectory(fileData.FullName);
930                 }
931                 catch { } // Ignore all exceptions
932
933                 continue;
934             }
935
936             // VSWhidbey 564168 Do not delete files that cannot be deleted, these files are still
937             // referenced by other appdomains that are in the process of shutting down.
938             // We also do not rename as renaming can cause an assembly not to be found if another
939             // appdomain tries to compile against it.
940             DiskBuildResultCache.TryDeleteFile(fileData.FullName);
941
942         }
943
944
945         // Clean up the fusion shadow copy cache
946
947         AppDomainSetup appDomainSetup = Thread.GetDomain().SetupInformation;
948         UnsafeNativeMethods.DeleteShadowCache(appDomainSetup.CachePath,
949             appDomainSetup.ApplicationName);
950     }
951
952     // Deletes all files in the directory, but leaves the directory there
953     internal void DeleteFilesInDirectory(string path) {
954         foreach (FileData fileData in FileEnumerator.Create(path)) {
955             if (fileData.IsDirectory) {
956                 Directory.Delete(fileData.FullName, true /*recursive*/);
957                 continue;
958             }
959             Util.RemoveOrRenameFile(fileData.FullName);
960         }
961     }
962 }
963
964 internal abstract class PrecompBaseDiskBuildResultCache: DiskBuildResultCache {
965
966     // In precompilation, the preservation files go in the bin directory
967     internal PrecompBaseDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
968 }
969
970 // Used when precompiling a site
971 internal class PrecompilerDiskBuildResultCache: PrecompBaseDiskBuildResultCache {
972
973     internal PrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) {
974
975         EnsureDiskCacheDirectoryCreated();
976     }
977 }
978
979 // Used when precompiling a site using updatable precompilation
980 internal class UpdatablePrecompilerDiskBuildResultCache: PrecompilerDiskBuildResultCache {
981
982     internal UpdatablePrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
983
984     internal override void CacheBuildResult(string cacheKey, BuildResult result,
985         long hashCode, DateTime utcStart) {
986
987         // Don't create preservation files in bin for pages in the updatable model,
988         // because we turn them into a v1 style code behind, which works as a result of
989         // having the aspx file point to the bin class via an inherits attribute.
990         if (result is BuildResultCompiledTemplateType)
991             return;
992
993         base.CacheBuildResult(cacheKey, result, hashCode, utcStart);
994     }
995
996 }
997
998 // Used when a site is already precompiled
999 internal class PrecompiledSiteDiskBuildResultCache: PrecompBaseDiskBuildResultCache {
1000
1001     internal PrecompiledSiteDiskBuildResultCache(string cacheDir) : base(cacheDir) {}
1002
1003     protected override bool PrecompilationMode { get { return true; } }
1004
1005     internal override void CacheBuildResult(string cacheKey, BuildResult result,
1006         long hashCode, DateTime utcStart) {
1007
1008         // Nothing to cache to disk if the app is already precompiled
1009     }
1010
1011     internal override void RemoveAssemblyAndRelatedFiles(string baseName) {
1012         // Never remove precompiled files (we couldn't anyways since they're
1013         // in the app dir)
1014     }
1015 }
1016
1017 }