Updates referencesource to .NET 4.7
[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 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
76     internal MemoryBuildResultCache() {
77
78         // Register an AssemblyLoad event
79         AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(OnAssemblyLoad);
80     }
81
82     [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
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
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
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)]
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
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.
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
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
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
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
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
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
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
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
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
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
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      */
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; } }
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      */
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
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
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
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
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.
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.
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
666     internal static bool HasDotDeleteFile(string s) {
667         return File.Exists(s + dotDelete);
668     }
669
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
694     internal StandardDiskBuildResultCache(string cacheDir)
695         : base(cacheDir) {
696
697         Debug.Assert(cacheDir == HttpRuntime.CodegenDirInternal);
698
699         EnsureDiskCacheDirectoryCreated();
700
701         FindSatelliteDirectories();
702     }
703
704     private string GetSpecialFilesCombinedHashFileName() {
705         return BuildManager.WebHashFilePath;
706     }
707
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      */
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
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      */
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
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
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      */
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
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.")]
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
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
962     internal PrecompBaseDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
963 }
964
965 // Used when precompiling a site
966 internal class PrecompilerDiskBuildResultCache: PrecompBaseDiskBuildResultCache {
967
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
977     internal UpdatablePrecompilerDiskBuildResultCache(string cacheDir) : base(cacheDir) { }
978
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
996     internal PrecompiledSiteDiskBuildResultCache(string cacheDir) : base(cacheDir) {}
997
998     protected override bool PrecompilationMode { get { return true; } }
999
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
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 }