// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; using System.Threading; using System.Runtime.Serialization; using Tools.DotNETCommon; using System.Collections.Concurrent; namespace UnrealBuildTool { /// /// Represents a file on disk that is used as an input or output of a build action. FileItem instances are unique for a given path. Use FileItem.GetItemByFileReference /// to get the FileItem for a specific path. /// class FileItem { /// /// The directory containing this file /// DirectoryItem CachedDirectory; /// /// Location of this file /// public readonly FileReference Location; /// /// The information about the file. /// FileInfo Info; /// /// A case-insensitive dictionary that's used to map each unique file name to a single FileItem object. /// static ConcurrentDictionary UniqueSourceFileMap = new ConcurrentDictionary(); /// /// Constructor /// /// Location of the file /// File info private FileItem(FileReference Location, FileInfo Info) { this.Location = Location; this.Info = Info; } /// /// Name of this file /// public string Name { get { return Info.Name; } } /// /// Full name of this file /// public string FullName { get { return Location.FullName; } } /// /// Accessor for the absolute path to the file /// public string AbsolutePath { get { return Location.FullName; } } /// /// Gets the directory that this file is in /// public DirectoryItem Directory { get { if(CachedDirectory == null) { CachedDirectory = DirectoryItem.GetItemByDirectoryReference(Location.Directory); } return CachedDirectory; } } /// /// Whether the file exists. /// public bool Exists { get { return Info.Exists; } } /// /// Size of the file if it exists, otherwise -1 /// public long Length { get { return Info.Length; } } /// /// The attributes for this file /// public FileAttributes Attributes { get { return Info.Attributes; } } /// /// The last write time of the file. /// public DateTime LastWriteTimeUtc { get { return Info.LastWriteTimeUtc; } } /// /// Determines if the file has the given extension /// /// The extension to check for /// True if the file has the given extension, false otherwise public bool HasExtension(string Extension) { return Location.HasExtension(Extension); } /// /// Gets the directory containing this file /// /// DirectoryItem for the directory containing this file public DirectoryItem GetDirectoryItem() { return Directory; } /// /// Updates the cached directory for this file. Used by DirectoryItem when enumerating files, to avoid having to look this up later. /// /// The directory that this file is in public void UpdateCachedDirectory(DirectoryItem Directory) { Debug.Assert(Directory.Location == Location.Directory); CachedDirectory = Directory; } /// /// Gets a FileItem corresponding to the given path /// /// Path for the FileItem /// The FileItem that represents the given file path. public static FileItem GetItemByPath(string FilePath) { return GetItemByFileReference(new FileReference(FilePath)); } /// /// Gets a FileItem for a given path /// /// Information about the file /// The FileItem that represents the given a full file path. public static FileItem GetItemByFileInfo(FileInfo Info) { FileReference Location = new FileReference(Info); FileItem Result; if (!UniqueSourceFileMap.TryGetValue(Location, out Result)) { FileItem NewFileItem = new FileItem(Location, Info); if(UniqueSourceFileMap.TryAdd(Location, NewFileItem)) { Result = NewFileItem; } else { Result = UniqueSourceFileMap[Location]; } } return Result; } /// /// Gets a FileItem for a given path /// /// Location of the file /// The FileItem that represents the given a full file path. public static FileItem GetItemByFileReference(FileReference Location) { FileItem Result; if (!UniqueSourceFileMap.TryGetValue(Location, out Result)) { FileItem NewFileItem = new FileItem(Location, Location.ToFileInfo()); if(UniqueSourceFileMap.TryAdd(Location, NewFileItem)) { Result = NewFileItem; } else { Result = UniqueSourceFileMap[Location]; } } return Result; } /// /// Determines the appropriate encoding for a string: either ASCII or UTF-8. /// /// The string to test. /// Either System.Text.Encoding.ASCII or System.Text.Encoding.UTF8, depending on whether or not the string contains non-ASCII characters. private static Encoding GetEncodingForString(string Str) { // If the string length is equivalent to the encoded length, then no non-ASCII characters were present in the string. // Don't write BOM as it messes with clang when loading response files. return (Encoding.UTF8.GetByteCount(Str) == Str.Length) ? Encoding.ASCII : new UTF8Encoding(false); } /// /// Creates a text file with the given contents. If the contents of the text file aren't changed, it won't write the new contents to /// the file to avoid causing an action to be considered outdated. /// /// Path to the intermediate file to create /// Contents of the new file /// File item for the newly created file public static FileItem CreateIntermediateTextFile(FileReference Location, string Contents) { // Only write the file if its contents have changed. if (!FileReference.Exists(Location)) { DirectoryReference.CreateDirectory(Location.Directory); FileReference.WriteAllText(Location, Contents, GetEncodingForString(Contents)); } else { string CurrentContents = Utils.ReadAllText(Location.FullName); if(!String.Equals(CurrentContents, Contents, StringComparison.InvariantCultureIgnoreCase)) { FileReference BackupFile = new FileReference(Location.FullName + ".old"); try { Log.TraceLog("Updating {0}: contents have changed. Saving previous version to {1}.", Location, BackupFile); FileReference.Delete(BackupFile); FileReference.Move(Location, BackupFile); } catch(Exception Ex) { Log.TraceWarning("Unable to rename {0} to {1}", Location, BackupFile); Log.TraceLog("{0}", ExceptionUtils.FormatExceptionDetails(Ex)); } FileReference.WriteAllText(Location, Contents, GetEncodingForString(Contents)); } } // Reset the file info, in case it already knows about the old file FileItem Item = GetItemByFileReference(Location); Item.ResetCachedInfo(); return Item; } /// /// Creates a text file with the given contents. If the contents of the text file aren't changed, it won't write the new contents to /// the file to avoid causing an action to be considered outdated. /// /// Path to the intermediate file to create /// Contents of the new file /// File item for the newly created file public static FileItem CreateIntermediateTextFile(FileReference AbsolutePath, IEnumerable Contents) { return CreateIntermediateTextFile(AbsolutePath, string.Join(Environment.NewLine, Contents)); } /// /// Deletes the file. /// public void Delete() { Debug.Assert(Exists); int MaxRetryCount = 3; int DeleteTryCount = 0; bool bFileDeletedSuccessfully = false; do { // If this isn't the first time through, sleep a little before trying again if (DeleteTryCount > 0) { Thread.Sleep(1000); } DeleteTryCount++; try { // Delete the destination file if it exists FileInfo DeletedFileInfo = new FileInfo(AbsolutePath); if (DeletedFileInfo.Exists) { DeletedFileInfo.IsReadOnly = false; DeletedFileInfo.Delete(); } // Success! bFileDeletedSuccessfully = true; } catch (Exception Ex) { Log.TraceInformation("Failed to delete file '" + AbsolutePath + "'"); Log.TraceInformation(" Exception: " + Ex.Message); if (DeleteTryCount < MaxRetryCount) { Log.TraceInformation("Attempting to retry..."); } else { Log.TraceInformation("ERROR: Exhausted all retries!"); } } } while (!bFileDeletedSuccessfully && (DeleteTryCount < MaxRetryCount)); } /// /// Resets the cached file info /// public void ResetCachedInfo() { Info = Location.ToFileInfo(); } /// /// Resets all cached file info. Significantly reduces performance; do not use unless strictly necessary. /// public static void ResetAllCachedInfo_SLOW() { foreach(FileItem Item in UniqueSourceFileMap.Values) { Item.ResetCachedInfo(); } } /// /// Return the path to this FileItem to debugging /// /// Absolute path to this file item public override string ToString() { return AbsolutePath; } } /// /// Helper functions for serialization /// static class FileItemExtensionMethods { /// /// Read a file item from a binary archive /// /// Reader to serialize data from /// Instance of the serialized file item public static FileItem ReadFileItem(this BinaryArchiveReader Reader) { return Reader.ReadObjectReference(() => FileItem.GetItemByFileReference(Reader.ReadFileReference())); } /// /// Write a file item to a binary archive /// /// Writer to serialize data to /// File item to write public static void WriteFileItem(this BinaryArchiveWriter Writer, FileItem FileItem) { Writer.WriteObjectReference(FileItem, () => Writer.WriteFileReference(FileItem.Location)); } /// /// Read a file item as a DirectoryItem and name. This is slower than reading it directly, but results in a significantly smaller archive /// where most files are in the same directories. /// /// Archive to read from /// FileItem read from the archive static FileItem ReadCompactFileItemData(this BinaryArchiveReader Reader) { DirectoryItem Directory = Reader.ReadDirectoryItem(); string Name = Reader.ReadString(); FileItem FileItem = FileItem.GetItemByFileReference(FileReference.Combine(Directory.Location, Name)); FileItem.UpdateCachedDirectory(Directory); return FileItem; } /// /// Read a file item in a format which de-duplicates directory names. /// /// Reader to serialize data from /// Instance of the serialized file item public static FileItem ReadCompactFileItem(this BinaryArchiveReader Reader) { return Reader.ReadObjectReference(() => ReadCompactFileItemData(Reader)); } /// /// Writes a file item in a format which de-duplicates directory names. /// /// Writer to serialize data to /// File item to write public static void WriteCompactFileItem(this BinaryArchiveWriter Writer, FileItem FileItem) { Writer.WriteObjectReference(FileItem, () => { Writer.WriteDirectoryItem(FileItem.GetDirectoryItem()); Writer.WriteString(FileItem.Name); }); } } }