// 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); });
}
}
}