Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/BuildGraph/TempStorage.cs
Ben Marsh 75fa4e9e6d Copying //UE4/Dev-Core to //UE4/Dev-Main (Source: //UE4/Dev-Core @ 3386108)
#rb none
#lockdown Nick.Penwarden

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3345860 on 2017/03/14 by Daniel.Lamb

	Fixed crash when building DLC

	#test Cook paragon.

Change 3347324 on 2017/03/15 by Gil.Gribb

	UE4 - Removed old code relating to FAsyncArchive, FAsyncIOSubsystemBase and package level compression. The editor now uses the lowest levels on the new async IO scheme.

Change 3347331 on 2017/03/15 by Robert.Manuszewski

	Fix for a crash caused by GC killing BP class (due to no strong references) but its CDO is being kept alive because it was in the same cluster as the class and was not marked as pending kill.

	#jira UE-42732

Change 3347371 on 2017/03/15 by Graeme.Thornton

	Fix for runtime asset cache not invalidating files with an outdated version number

Change 3349161 on 2017/03/16 by Steve.Robb

	Generated UFUNCTION FNames no longer exported.
	Misc refactors of code generation.

Change 3349167 on 2017/03/16 by Steve.Robb

	Unused TBoolConstant removed (the more general TIntegralConstant should be used instead).

Change 3349274 on 2017/03/16 by Gil.Gribb

	UE4 - Fix loading a package that is already loaded.

Change 3349534 on 2017/03/16 by Ben.Marsh

	UBT: Check that the SN-DBS service is running before attempting to use it.

Change 3349612 on 2017/03/16 by Gil.Gribb

	UE4 - Increased estimate of summary size.

Change 3350021 on 2017/03/16 by Gil.Gribb

	UE4 - Fixed crash in signature checks when mounting pak files.

Change 3350052 on 2017/03/16 by Ben.Marsh

	Remove invalid characters from macro names before passing as macro values. Prevents compile errors for projects which have apostrophes in the name.

Change 3350360 on 2017/03/16 by Ben.Marsh

	UAT: Fix non-threadsafe access of ExeToTimeInMs when spawning external processes.

Change 3351670 on 2017/03/17 by Ben.Marsh

	UBT: Ignore all default libraries when creating import libs. Sometimes #pragma comment(lib, ...) directives can add force additional libraries onto the linker/librarian command line. We don't want or need these included when generating import libraries, but they can cause errors due to search paths not being able to find them.

Change 3352289 on 2017/03/17 by Ben.Marsh

	Fix issues working with > 2GB archives caused by truncation of the return value from FArchive::Tell() down to 32-bits.

Change 3352390 on 2017/03/17 by Ben.Marsh

	Remove unused/out of date binaries for CrashReporter.

Change 3352392 on 2017/03/17 by Ben.Marsh

	Remove UnrealDocTool binaries. This is distributed through a Visual Studio plugin now.

Change 3352410 on 2017/03/17 by Ben.Marsh

	Remove P4ChangeReporter. I don't believe this is used any more.

Change 3352450 on 2017/03/17 by Ben.Marsh

	Disable including CrashReporter by default when packaging projects. This is only useful with a CrashReporter backend set up, which only usually applies to Epic internal projects.

Change 3352455 on 2017/03/17 by Ben.Marsh

	Remove RegisterPII and TranslatedWordsCountEstimator executables. Don't believe these are used any more.

Change 3352940 on 2017/03/17 by Wes.Hunt

	Update CRP to not send Slack queue size updates unless the waiting time is greater  than 1 minute.
	#codereview: jin.zhang

Change 3353658 on 2017/03/20 by Steve.Robb

	Fix for crash when importing a BP which has a populated TMap with an enum class key.

Change 3354056 on 2017/03/20 by Steve.Robb

	TAssetPtr<T> can now be constructed from a nullptr without a full definition of T.

Change 3356111 on 2017/03/21 by Graeme.Thornton

	Fix for UE-34131
	 - Support double and fname stat types in UFE stat export to CSV

	#jira UE-34131

Change 3358584 on 2017/03/22 by Daniel.Lamb

	Fixed the garbage collection keep flags when cleaning the sandbox for iterative cooking.

	#test Cook shootergame

Change 3360379 on 2017/03/23 by Gil.Gribb

	UE4 - Avoid adding a linker annotation if it actually hasn't changed. Improves ConditionalBeginDestroy performance.

Change 3360623 on 2017/03/23 by Gil.Gribb

	UE4 - Change from MarcA to avoid a redudnant removal of PrimitiveComponent from the streaming managers during ConditionalBeginDestroy.

Change 3360627 on 2017/03/23 by Gil.Gribb

	UE4 - Optimized UObject hash tables for speed and space.

Change 3361183 on 2017/03/23 by Gil.Gribb

	UE4 - Fixed change to NotifyPrimitiveDetached so that it works in the editor.

Change 3361906 on 2017/03/23 by Steve.Robb

	Fix for a bad hint index when instantiating map property subobjects when the defaults has fewer but non-zero elements.

	#jira UE-43272

Change 3362839 on 2017/03/24 by Gil.Gribb

	UE4 - Fixed hash table lock optimization.

Change 3367348 on 2017/03/28 by Robert.Manuszewski

	Making sure streamed-in SoundWaves get added to GC clusters.

Change 3367386 on 2017/03/28 by Ben.Marsh

	EC: Pass the Semaphores property from a build type as a parameter to new build jobs.

Change 3367422 on 2017/03/28 by Ben.Marsh

	EC: Allow limiting the number of scheduled jobs that will be automatically run at a particular time. Each build type can have a 'Semaphores' property in the branch settings file, which will be copied to newly created jobs. Before scheduling new jobs, EC is queried for the 'Semaphores' property on any running jobs, and build types with existing semaphores will be skipped. Does not prevent jobs from being run manually.

Change 3367469 on 2017/03/28 by Ben.Marsh

	EC: Prevent multiple incremental jobs running at once.

Change 3367640 on 2017/03/28 by Ben.Marsh

	Plugins: Add an optional EngineVersion field back into the plugin descriptor. If set, the engine will warn if the plugin is not compatible with the current engine version. Plugins will set this field by default when packaging; pass -Unversioned to override.

Change 3367836 on 2017/03/28 by Uriel.Doyon

	Improved handled of references in the streaming manager

Change 3369354 on 2017/03/29 by Graeme.Thornton

	Added AES encrypt/decrypt functions that take a byte array for the key

Change 3369804 on 2017/03/29 by Ben.Marsh

	Remove incorrect "EngineVersion" settings from plugin descriptors.

Change 3370462 on 2017/03/29 by Ben.Marsh

	Editor: Install Visual Studio 2017 by default, instead of Visual Studio 2015. Changed to use ExecElevatedProcess() to prevent installer failing to run if the current user is not already an administrator.

	#jira UE-43467

Change 3371598 on 2017/03/30 by Ben.Marsh

	UBT: Fix message for missing toolchain in VS2017.

Change 3372827 on 2017/03/30 by Ben.Marsh

	BuildGraph: Output an error at the end of each step if any previous build products have been modified.

Change 3372947 on 2017/03/30 by Ben.Marsh

	[Merge] Always add the host editor platform as supported in an installed build. Not doing so prevents the build platform being registered in UBT, which prevents doing any platform-specific staging operations in UAT.

Change 3372958 on 2017/03/30 by Ben.Marsh

	[Merge] Simplify log output for cooks. Suppress additional timestamps from the editor when running through UAT.

Change 3372981 on 2017/03/30 by Ben.Marsh

	[Merge] Modular game fixes for UAT

	* Store list of executable names from the receipts instead of generating them from Target/Platform/Config/Architecture combination
	* Get full list of staged executables from receipts instead of assuming only non-code projects are in Engine
	* Always pass short project name as Bootstrap argument, so that modular game exe knows which project to start

Change 3373024 on 2017/03/30 by Ben.Marsh

	[Merge] Add an option to UAT (-CookOutputDir=...) and the cooker (-OutputDir=...) which allows overriding the output directory for cooked files, and fix situations where the directory becomes too deep.

Change 3373041 on 2017/03/30 by Ben.Marsh

	[Merge] Added UAT script to replace assets with another source
	Renamed ReplaceAssetsCommandlet to GenerateAssetsManifest as it now outputs a list of files and has nothing specific about replacing files

Change 3373052 on 2017/03/30 by Ben.Marsh

	[Merge] Changed CopyUsingDistillFileSet command so that it can use a pre-existing manifest file instead of running commandlet

Change 3373092 on 2017/03/30 by Ben.Marsh

	[Merge] Fixed crash attempting to load cooked static mesh in editor

Change 3373112 on 2017/03/30 by Ben.Marsh

	[Merge] Fixed crash caused by loading cooked StaticMesh in editor that didn't have any SourceModels

Change 3373132 on 2017/03/30 by Ben.Marsh

	[Merge] Added Additional Maps that are always cooked to the GenerateDistillFileSetsCommandlet

Change 3373138 on 2017/03/30 by Ben.Marsh

	[Merge] Fixed code issue with playback of cooked SoundCues
	Skip over code using editor only data when editor data has been stripped

Change 3373143 on 2017/03/30 by Ben.Marsh

	[Merge] Fixed crash when attempting to open multiple cooked assets

Change 3373156 on 2017/03/30 by Ben.Marsh

	[Merge] Added commandlet to replace game assets with those from another source (intended for cooked asset replacement)

Change 3373161 on 2017/03/30 by Ben.Marsh

	[Merge] Prevented crash by not attempting to Load Mips again if a package has cooked data

Change 3373168 on 2017/03/30 by Ben.Marsh

	[Merge] Fix output path for DLC pak file, so it can be discovered by the engine and automatically mounted (and to stop it colliding with the main game pak file).

Change 3373204 on 2017/03/30 by Ben.Marsh

	[Merge] Fix crash when switching levels in PIE, due to bulk data already having been discarded for cooked assets. Cooking sets BULKDATA_SingleUse for textures, but PIEing needs to keep bulk data around.

Change 3373209 on 2017/03/30 by Ben.Marsh

	[Merge] Fix missing material in mod editor for cooked assets.

Change 3373388 on 2017/03/30 by Ben.Marsh

	[Merge] Various improvements to the plugin browser and new plugin wizard from Robo Recall.

Change 3374200 on 2017/03/31 by Ben.Marsh

	[Merge] Latest OdinEditor plugin from //Odin/Main, to fix build failures. Re-made change to OdinUnrealEdEngine to remove dependencies on analytics.

Change 3374279 on 2017/03/31 by Ben.Marsh

	PR #3441: Invalid JSON in FeaturePacks (Contributed by projectgheist)

Change 3374331 on 2017/03/31 by Ben.Marsh

	UBT: Disable warning pragmas on Mono; not supported on current compiler.

	#jira UE-43451

Change 3375108 on 2017/03/31 by Ben.Marsh

	Removing another plugin EngineVersion property.

Change 3375126 on 2017/03/31 by Ben.Marsh

	Fix incorrect executable paths being generated for Windows.

Change 3375159 on 2017/03/31 by Graeme.Thornton

	Pak Index Encryption
	 - Added "-encryptindex" option to unrealpak which will encrypt the pak index, making the pak file unreadable without the associated decryption key
	 - Added "-encryptpakindex" option to UAT to force on index encryption
	 - Added "bEncryptPakIndex" setting to project packaging settings so pak encryption can be controlled via the editor

Change 3375197 on 2017/03/31 by Graeme.Thornton

	Enable pak index encryption in shootergame

Change 3375377 on 2017/03/31 by Ben.Marsh

	Add build node to submit updated UnrealPak binaries for Win64, Mac and Linux. Currently has to be run via a custom build on EC, with the target set to "Submit UnrealPak Binaries".

Change 3376418 on 2017/04/03 by Ben.Marsh

	BuildGraph: Always clear the cached node state when running locally without having to manually specify the -ClearHistory argument. The -Resume argument allows the previous behavior of continuing a previous build.

Change 3376447 on 2017/04/03 by Ben.Marsh

	Build: Remove some unused stream settings

Change 3376469 on 2017/04/03 by Ben.Marsh

	Build: Add a customizable field for the script to use for custom builds in every branch.

Change 3376654 on 2017/04/03 by Ben.Marsh

	Add a fatal error message containing the module with an outstanding reference when trying to unload it.

	#jira UE-42423

Change 3376747 on 2017/04/03 by Gil.Gribb

	UE4 - Fixed crash relating to FGenericAsyncReadFileHandle when not using the EDL.

Change 3377173 on 2017/04/03 by Ben.Marsh

	Make sure callstacks are written to stdout following a crash on a background thread.

Change 3377183 on 2017/04/03 by Ben.Marsh

	Removing support for building VS2013 targets. Ability to generate VS2013 project files is still allowed, but unsupported (via the -2013unsupported command line argument).

Change 3377280 on 2017/04/03 by Ben.Marsh

	Build: Post UGS badges for all UE4 development streams, with the project set to $(Branch)/...

Change 3377311 on 2017/04/03 by Ben.Marsh

	Build: Set the 'Semaphores' parameter for any jobs started from a schedule.

Change 3377326 on 2017/04/03 by Ben.Marsh

	UGS: Show badges which match an entire subtree if the project field ends with "...".

Change 3377392 on 2017/04/03 by Ben.Marsh

	Add badges to UE4/Main and UE4/Release streams, and change the names of the builds in development streams to distinguish them.

Change 3377895 on 2017/04/03 by Ben.Marsh

	EC: Send notification emails whenever UAT fails to compile.

Change 3377923 on 2017/04/03 by Ben.Marsh

	Build: Use a different semaphore for the common editors build target to the incremental compile build target.

Change 3378297 on 2017/04/04 by Graeme.Thornton

	Fix incorrect generation of UE_ENGINE_DIRECTORY in UBT

Change 3378301 on 2017/04/04 by Ben.Marsh

	UBT: Try enabling bAdaptiveUnityDisablesPCH by default, to reduce the number of build failures we see due to missing includes.

Change 3378460 on 2017/04/04 by Graeme.Thornton

	Remove dependency preloading system from sync and async loading paths

Change 3378535 on 2017/04/04 by Robert.Manuszewski

	Fix for audio crash when launching Ocean PIE after removing the audio chunk allocation in CL #3347324

	#jira UE-43544

Change 3378575 on 2017/04/04 by Robert.Manuszewski

	Making sure actor clusters are not created in non-cooked builds

	#jira UE-43617
	#jira UE-43614

Change 3378589 on 2017/04/04 by Robert.Manuszewski

	Disabling debug GC cluster logging

	#jira UE-43617

Change 3379118 on 2017/04/04 by Robert.Manuszewski

	Disabling actor clustering by default, keeping it on in Orion and Ocean

Change 3379815 on 2017/04/04 by Ben.Marsh

	Revert change to derive executable names from target receipts. While a better solution than making them up, Android relies on having the base executable names for supporting multiple architectures.

Change 3380811 on 2017/04/05 by Gil.Gribb

	UE4 - Put the special boot order things into baseengine.ini so that licensees and games can add to it.

Change 3383313 on 2017/04/06 by Uriel.Doyon

	Integrated CL 3372436 3372765 3373272 from Dev-Rendering
	#JIRA UE-43669

Change 3383531 on 2017/04/06 by Ben.Marsh

	UGS: Ignore failures when querying whether paths exist. Permissions can cause this folder to fail, even if it will succeed at a parent directory.

Change 3383786 on 2017/04/06 by Ben.Zeigler

	Back out changelist 3382694 and replace with CL #3383757 from bob.tellez:
	Fix memory stomping issue caused by removing a FFortProfileSynchronizeRequest from SynchronizeRequests in UFortRegisteredPlayerInfo::UpdateSynchronizeRequest before SynchronizeProfile had finished executing

Change 3385089 on 2017/04/07 by Gil.Gribb

	UE4 - Critical. Fixed memory leak in pak precacher.

[CL 3386123 by Ben Marsh in Main branch]
2017-04-10 11:00:33 -04:00

1324 lines
50 KiB
C#

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using UnrealBuildTool;
using AutomationTool;
namespace AutomationTool
{
/// <summary>
/// Stores the name of a temp storage block
/// </summary>
public class TempStorageBlock
{
/// <summary>
/// Name of the node
/// </summary>
[XmlAttribute]
public string NodeName;
/// <summary>
/// Name of the output from this node
/// </summary>
[XmlAttribute]
public string OutputName;
/// <summary>
/// Default constructor, for XML serialization.
/// </summary>
private TempStorageBlock()
{
}
/// <summary>
/// Construct a temp storage block
/// </summary>
/// <param name="InNodeName">Name of the node</param>
/// <param name="InOutputName">Name of the node's output</param>
public TempStorageBlock(string InNodeName, string InOutputName)
{
NodeName = InNodeName;
OutputName = InOutputName;
}
/// <summary>
/// Tests whether two temp storage blocks are equal
/// </summary>
/// <param name="Other">The object to compare against</param>
/// <returns>True if the blocks are equivalent</returns>
public override bool Equals(object Other)
{
TempStorageBlock OtherBlock = Other as TempStorageBlock;
return OtherBlock != null && NodeName == OtherBlock.NodeName && OutputName == OtherBlock.OutputName;
}
/// <summary>
/// Returns a hash code for this block name
/// </summary>
/// <returns>Hash code for the block</returns>
public override int GetHashCode()
{
return ToString().GetHashCode();
}
/// <summary>
/// Returns the name of this block for debugging purposes
/// </summary>
/// <returns>Name of this block as a string</returns>
public override string ToString()
{
return String.Format("{0}/{1}", NodeName, OutputName);
}
}
/// <summary>
/// Information about a single file in temp storage
/// </summary>
[DebuggerDisplay("{RelativePath}")]
public class TempStorageFile
{
/// <summary>
/// The path of the file, relative to the engine root. Stored using forward slashes.
/// </summary>
[XmlAttribute]
public string RelativePath;
/// <summary>
/// The last modified time of the file, in UTC ticks since the Epoch.
/// </summary>
[XmlAttribute]
public long LastWriteTimeUtcTicks;
/// <summary>
/// Length of the file
/// </summary>
[XmlAttribute]
public long Length;
/// <summary>
/// Default constructor, for XML serialization.
/// </summary>
private TempStorageFile()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="FileInfo">File to be added</param>
/// <param name="RootDir">Root directory to store paths relative to</param>
public TempStorageFile(FileInfo FileInfo, DirectoryReference RootDir)
{
// Check the file exists and is in the right location
FileReference File = new FileReference(FileInfo);
if(!File.IsUnderDirectory(RootDir))
{
throw new AutomationException("Attempt to add file to temp storage manifest that is outside the root directory ({0})", File.FullName);
}
if(!FileInfo.Exists)
{
throw new AutomationException("Attempt to add file to temp storage manifest that does not exist ({0})", File.FullName);
}
RelativePath = File.MakeRelativeTo(RootDir).Replace(Path.DirectorySeparatorChar, '/');
LastWriteTimeUtcTicks = FileInfo.LastWriteTimeUtc.Ticks;
Length = FileInfo.Length;
}
/// <summary>
/// Compare stored for this file with the one on disk, and output an error if they differ.
/// </summary>
/// <param name="RootDir">Root directory for this branch</param>
/// <returns>True if the files are identical, false otherwise</returns>
public bool Compare(DirectoryReference RootDir)
{
string Message;
if(CompareInternal(RootDir, out Message))
{
if(Message != null)
{
CommandUtils.Log(Message);
}
return true;
}
else
{
if(Message != null)
{
CommandUtils.LogError(Message);
}
return false;
}
}
/// <summary>
/// Compare stored for this file with the one on disk, and output an error if they differ.
/// </summary>
/// <param name="RootDir">Root directory for this branch</param>
/// <returns>True if the files are identical, false otherwise</returns>
public bool CompareSilent(DirectoryReference RootDir)
{
string Message;
return CompareInternal(RootDir, out Message);
}
/// <summary>
/// Compare stored for this file with the one on disk, and output an error if they differ.
/// </summary>
/// <param name="RootDir">Root directory for this branch</param>
/// <param name="Message">Message describing the difference</param>
/// <returns>True if the files are identical, false otherwise</returns>
bool CompareInternal(DirectoryReference RootDir, out string Message)
{
FileReference LocalFile = ToFileReference(RootDir);
// Get the local file info, and check it exists
FileInfo Info = new FileInfo(LocalFile.FullName);
if(!Info.Exists)
{
Message = String.Format("Missing file from manifest - {0}", RelativePath);
return false;
}
// Check the size matches
if(Info.Length != Length)
{
Message = String.Format("File size differs from manifest - {0} is {1} bytes, expected {2} bytes", RelativePath, Info.Length, Length);
return false;
}
// Check the timestamp of the file matches. On FAT filesystems writetime has a two seconds resolution (see http://msdn.microsoft.com/en-us/library/windows/desktop/ms724290%28v=vs.85%29.aspx)
TimeSpan TimeDifference = new TimeSpan(Info.LastWriteTimeUtc.Ticks - LastWriteTimeUtcTicks);
if(TimeDifference.TotalSeconds < -2 || TimeDifference.TotalSeconds > +2)
{
DateTime ExpectedLocal = new DateTime(LastWriteTimeUtcTicks, DateTimeKind.Utc).ToLocalTime();
if(RequireMatchingTimestamps())
{
Message = String.Format("File date/time mismatch for {0} - was {1}, expected {2}, TimeDifference {3}", RelativePath, Info.LastWriteTime, ExpectedLocal, TimeDifference);
return false;
}
else
{
Message = String.Format("Ignored file date/time mismatch for {0} - was {1}, expected {2}, TimeDifference {3}", RelativePath, Info.LastWriteTime, ExpectedLocal, TimeDifference);
return true;
}
}
else
{
Message = null;
return true;
}
}
/// <summary>
/// Whether we should compare timestamps for this file. Some build products are harmlessly overwritten as part of the build process, so we flag those here.
/// </summary>
/// <returns>True if we should compare the file's timestamp, false otherwise</returns>
bool RequireMatchingTimestamps()
{
return RelativePath.IndexOf("/Binaries/DotNET/", StringComparison.InvariantCultureIgnoreCase) == -1 && RelativePath.IndexOf("/Binaries/Mac/", StringComparison.InvariantCultureIgnoreCase) == -1;
}
/// <summary>
/// Gets a local file reference for this file, given a root directory to base it from.
/// </summary>
/// <param name="RootDir">The local root directory</param>
/// <returns>Reference to the file</returns>
public FileReference ToFileReference(DirectoryReference RootDir)
{
return FileReference.Combine(RootDir, RelativePath.Replace('/', Path.DirectorySeparatorChar));
}
}
/// <summary>
/// Information about a single file in temp storage
/// </summary>
[DebuggerDisplay("{Name}")]
public class TempStorageZipFile
{
/// <summary>
/// Name of this file, including extension
/// </summary>
[XmlAttribute]
public string Name;
/// <summary>
/// Length of the file in bytes
/// </summary>
[XmlAttribute]
public long Length;
/// <summary>
/// Default constructor, for XML serialization
/// </summary>
private TempStorageZipFile()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="Info">FileInfo for the zip file</param>
public TempStorageZipFile(FileInfo Info)
{
Name = Info.Name;
Length = Info.Length;
}
}
/// <summary>
/// A manifest storing information about build products for a node's output
/// </summary>
public class TempStorageManifest
{
/// <summary>
/// List of output files
/// </summary>
[XmlArray]
[XmlArrayItem("File")]
public TempStorageFile[] Files;
/// <summary>
/// List of compressed archives containing the given files
/// </summary>
[XmlArray]
[XmlArrayItem("ZipFile")]
public TempStorageZipFile[] ZipFiles;
/// <summary>
/// Construct a static Xml serializer to avoid throwing an exception searching for the reflection info at runtime
/// </summary>
static XmlSerializer Serializer = XmlSerializer.FromTypes(new Type[]{ typeof(TempStorageManifest) })[0];
/// <summary>
/// Construct an empty temp storage manifest
/// </summary>
private TempStorageManifest()
{
}
/// <summary>
/// Creates a manifest from a flat list of files (in many folders) and a BaseFolder from which they are rooted.
/// </summary>
/// <param name="InFiles">List of full file paths</param>
/// <param name="RootDir">Root folder for all the files. All files must be relative to this RootDir.</param>
public TempStorageManifest(FileInfo[] InFiles, DirectoryReference RootDir)
{
Files = InFiles.Select(x => new TempStorageFile(x, RootDir)).ToArray();
}
/// <summary>
/// Gets the total size of the files stored in this manifest
/// </summary>
/// <returns>The total size of all files</returns>
public long GetTotalSize()
{
long Result = 0;
foreach(TempStorageFile File in Files)
{
Result += File.Length;
}
return Result;
}
/// <summary>
/// Load a manifest from disk
/// </summary>
/// <param name="File">File to load</param>
static public TempStorageManifest Load(FileReference File)
{
using(StreamReader Reader = new StreamReader(File.FullName))
{
return (TempStorageManifest)Serializer.Deserialize(Reader);
}
}
/// <summary>
/// Saves a manifest to disk
/// </summary>
/// <param name="File">File to save</param>
public void Save(FileReference File)
{
using(StreamWriter Writer = new StreamWriter(File.FullName))
{
Serializer.Serialize(Writer, this);
}
}
}
/// <summary>
/// Stores the contents of a tagged file set
/// </summary>
public class TempStorageFileList
{
/// <summary>
/// List of files that are in this tag set, relative to the root directory
/// </summary>
[XmlArray]
[XmlArrayItem("LocalFile")]
public string[] LocalFiles;
/// <summary>
/// List of files that are in this tag set, but not relative to the root directory
/// </summary>
[XmlArray]
[XmlArrayItem("LocalFile")]
public string[] ExternalFiles;
/// <summary>
/// List of referenced storage blocks
/// </summary>
[XmlArray]
[XmlArrayItem("Block")]
public TempStorageBlock[] Blocks;
/// <summary>
/// Construct a static Xml serializer to avoid throwing an exception searching for the reflection info at runtime
/// </summary>
static XmlSerializer Serializer = XmlSerializer.FromTypes(new Type[]{ typeof(TempStorageFileList) })[0];
/// <summary>
/// Construct an empty file list for deserialization
/// </summary>
private TempStorageFileList()
{
}
/// <summary>
/// Creates a manifest from a flat list of files (in many folders) and a BaseFolder from which they are rooted.
/// </summary>
/// <param name="InFiles">List of full file paths</param>
/// <param name="RootDir">Root folder for all the files. All files must be relative to this RootDir.</param>
/// <param name="InBlocks">Referenced storage blocks required for these files</param>
public TempStorageFileList(IEnumerable<FileReference> InFiles, DirectoryReference RootDir, IEnumerable<TempStorageBlock> InBlocks)
{
List<string> NewLocalFiles = new List<string>();
List<string> NewExternalFiles = new List<string>();
foreach(FileReference File in InFiles)
{
if(File.IsUnderDirectory(RootDir))
{
NewLocalFiles.Add(File.MakeRelativeTo(RootDir).Replace(Path.DirectorySeparatorChar, '/'));
}
else
{
NewExternalFiles.Add(File.FullName.Replace(Path.DirectorySeparatorChar, '/'));
}
}
LocalFiles = NewLocalFiles.ToArray();
ExternalFiles = NewExternalFiles.ToArray();
Blocks = InBlocks.ToArray();
}
/// <summary>
/// Load this list of files from disk
/// </summary>
/// <param name="File">File to load</param>
static public TempStorageFileList Load(FileReference File)
{
using(StreamReader Reader = new StreamReader(File.FullName))
{
return (TempStorageFileList)Serializer.Deserialize(Reader);
}
}
/// <summary>
/// Saves this list of files to disk
/// </summary>
/// <param name="File">File to save</param>
public void Save(FileReference File)
{
using(StreamWriter Writer = new StreamWriter(File.FullName))
{
Serializer.Serialize(Writer, this);
}
}
/// <summary>
/// Converts this file list into a set of FileReference objects
/// </summary>
/// <param name="RootDir">The root directory to rebase local files</param>
/// <returns>Set of files</returns>
public HashSet<FileReference> ToFileSet(DirectoryReference RootDir)
{
HashSet<FileReference> Files = new HashSet<FileReference>();
Files.UnionWith(LocalFiles.Select(x => FileReference.Combine(RootDir, x)));
Files.UnionWith(ExternalFiles.Select(x => new FileReference(x)));
return Files;
}
}
/// <summary>
/// Tracks the state of the current build job using the filesystem, allowing jobs to be restarted after a failure or expanded to include larger targets, and
/// providing a proxy for different machines executing parts of the build in parallel to transfer build products and share state as part of a build system.
///
/// If a shared temp storage directory is provided - typically a mounted path on a network share - all build products potentially needed as inputs by another node
/// are compressed and copied over, along with metadata for them (see TempStorageFile) and flags for build events that have occurred (see TempStorageEvent).
///
/// The local temp storage directory contains the same information, with the exception of the archived build products. Metadata is still kept to detect modified
/// build products between runs. If data is not present in local temp storage, it's retrieved from shared temp storage and cached in local storage.
/// </summary>
class TempStorage
{
/// <summary>
/// Root directory for this branch.
/// </summary>
DirectoryReference RootDir;
/// <summary>
/// The local temp storage directory (typically somewhere under /Engine/Saved directory).
/// </summary>
DirectoryReference LocalDir;
/// <summary>
/// The shared temp storage directory; typically a network location. May be null.
/// </summary>
DirectoryReference SharedDir;
/// <summary>
/// Whether to allow writes to shared storage
/// </summary>
bool bWriteToSharedStorage;
/// <summary>
/// Constructor
/// </summary>
/// <param name="InRootDir">Root directory for this branch</param>
/// <param name="InLocalDir">The local temp storage directory.</param>
/// <param name="InSharedDir">The shared temp storage directory. May be null.</param>
/// <param name="bInWriteToSharedStorage">Whether to write to shared storage, or only permit reads from it</param>
public TempStorage(DirectoryReference InRootDir, DirectoryReference InLocalDir, DirectoryReference InSharedDir, bool bInWriteToSharedStorage)
{
RootDir = InRootDir;
LocalDir = InLocalDir;
SharedDir = InSharedDir;
bWriteToSharedStorage = bInWriteToSharedStorage;
}
/// <summary>
/// Cleans all cached local state. We never remove shared storage.
/// </summary>
public void CleanLocal()
{
CommandUtils.DeleteDirectoryContents(LocalDir.FullName);
}
/// <summary>
/// Cleans local build products for a given node. Does not modify shared storage.
/// </summary>
/// <param name="NodeName">Name of the node</param>
public void CleanLocalNode(string NodeName)
{
DirectoryReference NodeDir = GetDirectoryForNode(LocalDir, NodeName);
if(DirectoryReference.Exists(NodeDir))
{
CommandUtils.DeleteDirectoryContents(NodeDir.FullName);
CommandUtils.DeleteDirectory_NoExceptions(NodeDir.FullName);
}
}
/// <summary>
/// Check whether the given node is complete
/// </summary>
/// <param name="NodeName">Name of the node</param>
/// <returns>True if the node is complete</returns>
public bool IsComplete(string NodeName)
{
// Check if it already exists locally
FileReference LocalFile = GetCompleteMarkerFile(LocalDir, NodeName);
if(FileReference.Exists(LocalFile))
{
return true;
}
// Check if it exists in shared storage
if(SharedDir != null)
{
FileReference SharedFile = GetCompleteMarkerFile(SharedDir, NodeName);
if(FileReference.Exists(SharedFile))
{
return true;
}
}
// Otherwise we don't have any data
return false;
}
/// <summary>
/// Mark the given node as complete
/// </summary>
/// <param name="NodeName">Name of the node</param>
public void MarkAsComplete(string NodeName)
{
// Create the marker locally
FileReference LocalFile = GetCompleteMarkerFile(LocalDir, NodeName);
DirectoryReference.CreateDirectory(LocalFile.Directory);
File.OpenWrite(LocalFile.FullName).Close();
// Create the marker in the shared directory
if(SharedDir != null && bWriteToSharedStorage)
{
FileReference SharedFile = GetCompleteMarkerFile(SharedDir, NodeName);
DirectoryReference.CreateDirectory(SharedFile.Directory);
File.OpenWrite(SharedFile.FullName).Close();
}
}
/// <summary>
/// Checks the integrity of the give node's local build products.
/// </summary>
/// <param name="NodeName">The node to retrieve build products for</param>
/// <param name="TagNames">List of tag names from this node.</param>
/// <returns>True if the node is complete and valid, false if not (and typically followed by a call to CleanNode()).</returns>
public bool CheckLocalIntegrity(string NodeName, IEnumerable<string> TagNames)
{
// If the node is not locally complete, fail immediately.
FileReference CompleteMarkerFile = GetCompleteMarkerFile(LocalDir, NodeName);
if(!FileReference.Exists(CompleteMarkerFile))
{
return false;
}
// Check that each of the tags exist
HashSet<TempStorageBlock> Blocks = new HashSet<TempStorageBlock>();
foreach(string TagName in TagNames)
{
// Check the local manifest exists
FileReference LocalFileListLocation = GetTaggedFileListLocation(LocalDir, NodeName, TagName);
if(!FileReference.Exists(LocalFileListLocation))
{
return false;
}
// Check the local manifest matches the shared manifest
if(SharedDir != null)
{
// Check the shared manifest exists
FileReference SharedFileListLocation = GetManifestLocation(SharedDir, NodeName, TagName);
if(!FileReference.Exists(SharedFileListLocation))
{
return false;
}
// Check the manifests are identical, byte by byte
byte[] LocalManifestBytes = File.ReadAllBytes(LocalFileListLocation.FullName);
byte[] SharedManifestBytes = File.ReadAllBytes(SharedFileListLocation.FullName);
if(!LocalManifestBytes.SequenceEqual(SharedManifestBytes))
{
return false;
}
}
// Read the manifest and add the referenced blocks to be checked
TempStorageFileList LocalFileList = TempStorageFileList.Load(LocalFileListLocation);
Blocks.UnionWith(LocalFileList.Blocks);
}
// Check that each of the outputs match
foreach(TempStorageBlock Block in Blocks)
{
// Check the local manifest exists
FileReference LocalManifestFile = GetManifestLocation(LocalDir, Block.NodeName, Block.OutputName);
if(!FileReference.Exists(LocalManifestFile))
{
return false;
}
// Check the local manifest matches the shared manifest
if(SharedDir != null)
{
// Check the shared manifest exists
FileReference SharedManifestFile = GetManifestLocation(SharedDir, Block.NodeName, Block.OutputName);
if(!FileReference.Exists(SharedManifestFile))
{
return false;
}
// Check the manifests are identical, byte by byte
byte[] LocalManifestBytes = File.ReadAllBytes(LocalManifestFile.FullName);
byte[] SharedManifestBytes = File.ReadAllBytes(SharedManifestFile.FullName);
if(!LocalManifestBytes.SequenceEqual(SharedManifestBytes))
{
return false;
}
}
// Read the manifest and check the files
TempStorageManifest LocalManifest = TempStorageManifest.Load(LocalManifestFile);
if(LocalManifest.Files.Any(x => !x.Compare(RootDir)))
{
return false;
}
}
return true;
}
/// <summary>
/// Reads a set of tagged files from disk
/// </summary>
/// <param name="NodeName">Name of the node which produced the tag set</param>
/// <param name="TagName">Name of the tag, with a '#' prefix</param>
/// <returns>The set of files</returns>
public TempStorageFileList ReadFileList(string NodeName, string TagName)
{
TempStorageFileList FileList;
// Try to read the tag set from the local directory
FileReference LocalFileListLocation = GetTaggedFileListLocation(LocalDir, NodeName, TagName);
if(FileReference.Exists(LocalFileListLocation))
{
CommandUtils.Log("Reading local file list from {0}", LocalFileListLocation.FullName);
FileList = TempStorageFileList.Load(LocalFileListLocation);
}
else
{
// Check we have shared storage
if(SharedDir == null)
{
throw new AutomationException("Missing local file list - {0}", LocalFileListLocation.FullName);
}
// Make sure the manifest exists
FileReference SharedFileListLocation = GetTaggedFileListLocation(SharedDir, NodeName, TagName);
if(!FileReference.Exists(SharedFileListLocation))
{
throw new AutomationException("Missing local or shared file list - {0}", SharedFileListLocation.FullName);
}
// Read the shared manifest
CommandUtils.Log("Copying shared tag set from {0} to {1}", SharedFileListLocation.FullName, LocalFileListLocation.FullName);
FileList = TempStorageFileList.Load(SharedFileListLocation);
// Save the manifest locally
DirectoryReference.CreateDirectory(LocalFileListLocation.Directory);
FileList.Save(LocalFileListLocation);
}
return FileList;
}
/// <summary>
/// Writes a list of tagged files to disk
/// </summary>
/// <param name="NodeName">Name of the node which produced the tag set</param>
/// <param name="TagName">Name of the tag, with a '#' prefix</param>
/// <param name="Files">List of files in this set</param>
/// <param name="Blocks">List of referenced storage blocks</param>
/// <returns>The set of files</returns>
public void WriteFileList(string NodeName, string TagName, IEnumerable<FileReference> Files, IEnumerable<TempStorageBlock> Blocks)
{
// Create the file list
TempStorageFileList FileList = new TempStorageFileList(Files, RootDir, Blocks);
// Save the set of files to the local and shared locations
FileReference LocalFileListLocation = GetTaggedFileListLocation(LocalDir, NodeName, TagName);
if(SharedDir != null && bWriteToSharedStorage)
{
FileReference SharedFileListLocation = GetTaggedFileListLocation(SharedDir, NodeName, TagName);
CommandUtils.Log("Saving file list to {0} and {1}", LocalFileListLocation.FullName, SharedFileListLocation.FullName);
DirectoryReference.CreateDirectory(SharedFileListLocation.Directory);
FileList.Save(SharedFileListLocation);
}
else
{
CommandUtils.Log("Saving file list to {0}", LocalFileListLocation.FullName);
}
// Save the local file list
DirectoryReference.CreateDirectory(LocalFileListLocation.Directory);
FileList.Save(LocalFileListLocation);
}
/// <summary>
/// Saves the given files (that should be rooted at the branch root) to a shared temp storage manifest with the given temp storage node and game.
/// </summary>
/// <param name="NodeName">The node which created the storage block</param>
/// <param name="BlockName">Name of the block to retrieve. May be null or empty.</param>
/// <param name="BuildProducts">Array of build products to be archived</param>
/// <param name="bPushToRemote">Allow skipping the copying of this manifest to shared storage, because it's not required by any other agent</param>
/// <returns>The created manifest instance (which has already been saved to disk).</returns>
public TempStorageManifest Archive(string NodeName, string BlockName, FileReference[] BuildProducts, bool bPushToRemote = true)
{
using(TelemetryStopwatch TelemetryStopwatch = new TelemetryStopwatch("StoreToTempStorage"))
{
// Create a manifest for the given build products
FileInfo[] Files = BuildProducts.Select(x => new FileInfo(x.FullName)).ToArray();
TempStorageManifest Manifest = new TempStorageManifest(Files, RootDir);
// Create the local directory for this node
DirectoryReference LocalNodeDir = GetDirectoryForNode(LocalDir, NodeName);
DirectoryReference.CreateDirectory(LocalNodeDir);
// Compress the files and copy to shared storage if necessary
bool bRemote = SharedDir != null && bPushToRemote && bWriteToSharedStorage;
if(bRemote)
{
// Create the shared directory for this node
FileReference SharedManifestFile = GetManifestLocation(SharedDir, NodeName, BlockName);
DirectoryReference.CreateDirectory(SharedManifestFile.Directory);
// Zip all the build products
FileInfo[] ZipFiles = ParallelZipFiles(Files, RootDir, SharedManifestFile.Directory, LocalNodeDir, SharedManifestFile.GetFileNameWithoutExtension());
Manifest.ZipFiles = ZipFiles.Select(x => new TempStorageZipFile(x)).ToArray();
// Save the shared manifest
CommandUtils.Log("Saving shared manifest to {0}", SharedManifestFile.FullName);
Manifest.Save(SharedManifestFile);
}
// Save the local manifest
FileReference LocalManifestFile = GetManifestLocation(LocalDir, NodeName, BlockName);
CommandUtils.Log("Saving local manifest to {0}", LocalManifestFile.FullName);
Manifest.Save(LocalManifestFile);
// Update the stats
long ZipFilesTotalSize = (Manifest.ZipFiles == null)? 0 : Manifest.ZipFiles.Sum(x => x.Length);
TelemetryStopwatch.Finish(string.Format("StoreToTempStorage.{0}.{1}.{2}.{3}.{4}.{5}.{6}", Files.Length, Manifest.GetTotalSize(), ZipFilesTotalSize, bRemote? "Remote" : "Local", 0, 0, BlockName));
return Manifest;
}
}
/// <summary>
/// Retrieve an output of the given node. Fetches and decompresses the files from shared storage if necessary, or validates the local files.
/// </summary>
/// <param name="NodeName">The node which created the storage block</param>
/// <param name="OutputName">Name of the block to retrieve. May be null or empty.</param>
/// <returns>Manifest of the files retrieved</returns>
public TempStorageManifest Retreive(string NodeName, string OutputName)
{
using(var TelemetryStopwatch = new TelemetryStopwatch("RetrieveFromTempStorage"))
{
// Get the path to the local manifest
FileReference LocalManifestFile = GetManifestLocation(LocalDir, NodeName, OutputName);
bool bLocal = FileReference.Exists(LocalManifestFile);
// Read the manifest, either from local storage or shared storage
TempStorageManifest Manifest;
if(bLocal)
{
CommandUtils.Log("Reading shared manifest from {0}", LocalManifestFile.FullName);
Manifest = TempStorageManifest.Load(LocalManifestFile);
}
else
{
// Check we have shared storage
if(SharedDir == null)
{
throw new AutomationException("Missing local manifest for node - {0}", LocalManifestFile.FullName);
}
// Get the shared directory for this node
FileReference SharedManifestFile = GetManifestLocation(SharedDir, NodeName, OutputName);
// Make sure the manifest exists
if(!FileReference.Exists(SharedManifestFile))
{
throw new AutomationException("Missing local or shared manifest for node - {0}", SharedManifestFile.FullName);
}
// Read the shared manifest
CommandUtils.Log("Copying shared manifest from {0} to {1}", SharedManifestFile.FullName, LocalManifestFile.FullName);
Manifest = TempStorageManifest.Load(SharedManifestFile);
// Unzip all the build products
DirectoryReference SharedNodeDir = GetDirectoryForNode(SharedDir, NodeName);
FileInfo[] ZipFiles = Manifest.ZipFiles.Select(x => new FileInfo(FileReference.Combine(SharedNodeDir, x.Name).FullName)).ToArray();
ParallelUnzipFiles(ZipFiles, RootDir);
// Fix any Unix permissions/chmod issues, and update the timestamps to match the manifest. Zip files only use local time, and there's no guarantee it matches the local clock.
foreach(TempStorageFile ManifestFile in Manifest.Files)
{
FileReference File = ManifestFile.ToFileReference(RootDir);
if (Utils.IsRunningOnMono)
{
CommandUtils.FixUnixFilePermissions(File.FullName);
}
System.IO.File.SetLastWriteTimeUtc(File.FullName, new DateTime(ManifestFile.LastWriteTimeUtcTicks, DateTimeKind.Utc));
}
// Save the manifest locally
DirectoryReference.CreateDirectory(LocalManifestFile.Directory);
Manifest.Save(LocalManifestFile);
}
// Check all the local files are as expected
bool bAllMatch = true;
foreach(TempStorageFile File in Manifest.Files)
{
bAllMatch &= File.Compare(RootDir);
}
if(!bAllMatch)
{
throw new AutomationException("Files have been modified");
}
// Update the stats and return
TelemetryStopwatch.Finish(string.Format("RetrieveFromTempStorage.{0}.{1}.{2}.{3}.{4}.{5}.{6}", Manifest.Files.Length, Manifest.Files.Sum(x => x.Length), bLocal? 0 : Manifest.ZipFiles.Sum(x => x.Length), bLocal? "Local" : "Remote", 0, 0, OutputName));
return Manifest;
}
}
/// <summary>
/// Zips a set of files (that must be rooted at the given RootDir) to a set of zip files in the given OutputDir. The files will be prefixed with the given basename.
/// </summary>
/// <param name="InputFiles">Fully qualified list of files to zip (must be rooted at RootDir).</param>
/// <param name="RootDir">Root Directory where all files will be extracted.</param>
/// <param name="OutputDir">Location to place the set of zip files created.</param>
/// <param name="StagingDir">Location to create zip files before copying them to the OutputDir. If the OutputDir is on a remote file share, staging may be more efficient. Use null to avoid using a staging copy.</param>
/// <param name="ZipBaseName">The basename of the set of zip files.</param>
/// <returns>Some metrics about the zip process.</returns>
/// <remarks>
/// This function tries to zip the files in parallel as fast as it can. It makes no guarantees about how many zip files will be created or which files will be in which zip,
/// but it does try to reasonably balance the file sizes.
/// </remarks>
private static FileInfo[] ParallelZipFiles(FileInfo[] InputFiles, DirectoryReference RootDir, DirectoryReference OutputDir, DirectoryReference StagingDir, string ZipBaseName)
{
// First get the sizes of all the files. We won't parallelize if there isn't enough data to keep the number of zips down.
var FilesInfo = InputFiles
.Select(InputFile => new { File = new FileReference(InputFile), FileSize = InputFile.Length })
.ToList();
// Profiling results show that we can zip 100MB quite fast and it is not worth parallelizing that case and creating a bunch of zips that are relatively small.
const long MinFileSizeToZipInParallel = 1024 * 1024 * 100L;
var bZipInParallel = FilesInfo.Sum(FileInfo => FileInfo.FileSize) >= MinFileSizeToZipInParallel;
// order the files in descending order so our threads pick up the biggest ones first.
// We want to end with the smaller files to more effectively fill in the gaps
var FilesToZip = new ConcurrentQueue<FileReference>(FilesInfo.OrderByDescending(FileInfo => FileInfo.FileSize).Select(FileInfo => FileInfo.File));
// We deliberately avoid Parallel.ForEach here because profiles have shown that dynamic partitioning creates
// too many zip files, and they can be of too varying size, creating uneven work when unzipping later,
// as ZipFile cannot unzip files in parallel from a single archive.
// We can safely assume the build system will not be doing more important things at the same time, so we simply use all our logical cores,
// which has shown to be optimal via profiling, and limits the number of resulting zip files to the number of logical cores.
//
// Sadly, mono implementation of System.IO.Compression is really poor (as of 2015/Aug), causing OOM when parallel zipping a large set of files.
// However, Ionic is MUCH slower than .NET's native implementation (2x+ slower in our build farm), so we stick to the faster solution on PC.
// The code duplication in the threadprocs is unfortunate here, and hopefully we can settle on .NET's implementation on both platforms eventually.
List<Thread> ZipThreads;
ConcurrentBag<FileInfo> ZipFiles = new ConcurrentBag<FileInfo>();
DirectoryReference ZipDir = StagingDir ?? OutputDir;
if (Utils.IsRunningOnMono)
{
ZipThreads = (
from CoreNum in Enumerable.Range(0, bZipInParallel ? Environment.ProcessorCount : 1)
let ZipFileName = FileReference.Combine(ZipDir, string.Format("{0}{1}.zip", ZipBaseName, bZipInParallel ? "-" + CoreNum.ToString("00") : ""))
select new Thread(() =>
{
// don't create the zip unless we have at least one file to add
FileReference File;
if (FilesToZip.TryDequeue(out File))
{
// Create one zip per thread using the given basename
using (var ZipArchive = new Ionic.Zip.ZipFile(ZipFileName.FullName) { CompressionLevel = Ionic.Zlib.CompressionLevel.BestSpeed })
{
// pull from the queue until we are out of files.
do
{
// use fastest compression. In our best case we are CPU bound, so this is a good tradeoff,
// cutting overall time by 2/3 while only modestly increasing the compression ratio (22.7% -> 23.8% for RootEditor PDBs).
// This is in cases of a super hot cache, so the operation was largely CPU bound.
ZipArchive.AddFile(File.FullName, CommandUtils.ConvertSeparators(PathSeparator.Slash, File.Directory.MakeRelativeTo(RootDir)));
} while (FilesToZip.TryDequeue(out File));
ZipArchive.Save();
}
// if we are using a staging dir, copy to the final location and delete the staged copy.
FileInfo ZipFile = new FileInfo(ZipFileName.FullName);
if (StagingDir != null)
{
FileInfo NewZipFile = ZipFile.CopyTo(CommandUtils.MakeRerootedFilePath(ZipFile.FullName, StagingDir.FullName, OutputDir.FullName));
ZipFile.Delete();
ZipFile = NewZipFile;
}
ZipFiles.Add(ZipFile);
}
})).ToList();
}
else
{
ZipThreads = (
from CoreNum in Enumerable.Range(0, bZipInParallel ? Environment.ProcessorCount : 1)
let ZipFileName = FileReference.Combine(ZipDir, string.Format("{0}{1}.zip", ZipBaseName, bZipInParallel ? "-" + CoreNum.ToString("00") : ""))
select new Thread(() =>
{
// don't create the zip unless we have at least one file to add
FileReference File;
if (FilesToZip.TryDequeue(out File))
{
// Create one zip per thread using the given basename
using (var ZipArchive = System.IO.Compression.ZipFile.Open(ZipFileName.FullName, System.IO.Compression.ZipArchiveMode.Create))
{
// pull from the queue until we are out of files.
do
{
// use fastest compression. In our best case we are CPU bound, so this is a good tradeoff,
// cutting overall time by 2/3 while only modestly increasing the compression ratio (22.7% -> 23.8% for RootEditor PDBs).
// This is in cases of a super hot cache, so the operation was largely CPU bound.
// Also, sadly, mono appears to have a bug where nothing you can do will properly set the LastWriteTime on the created entry,
// so we have to ignore timestamps on files extracted from a zip, since it may have been created on a Mac.
ZipFileExtensions.CreateEntryFromFile(ZipArchive, File.FullName, CommandUtils.ConvertSeparators(PathSeparator.Slash, File.MakeRelativeTo(RootDir)), System.IO.Compression.CompressionLevel.Fastest);
} while (FilesToZip.TryDequeue(out File));
}
// if we are using a staging dir, copy to the final location and delete the staged copy.
FileInfo ZipFile = new FileInfo(ZipFileName.FullName);
if (StagingDir != null)
{
FileInfo NewZipFile = ZipFile.CopyTo(CommandUtils.MakeRerootedFilePath(ZipFile.FullName, StagingDir.FullName, OutputDir.FullName));
ZipFile.Delete();
ZipFile = NewZipFile;
}
ZipFiles.Add(ZipFile);
}
})).ToList();
}
ZipThreads.ForEach(thread => thread.Start());
ZipThreads.ForEach(thread => thread.Join());
return ZipFiles.OrderBy(x => x.Name).ToArray();
}
/// <summary>
/// Unzips a set of zip files with a given basename in a given folder to a given RootDir.
/// </summary>
/// <param name="ZipFiles">Files to extract</param>
/// <param name="RootDir">Root Directory where all files will be extracted.</param>
/// <returns>Some metrics about the unzip process.</returns>
/// <remarks>
/// The code is expected to be the used as the symmetrical inverse of <see cref="ParallelZipFiles"/>, but could be used independently, as long as the files in the zip do not overlap.
/// </remarks>
private static void ParallelUnzipFiles(FileInfo[] ZipFiles, DirectoryReference RootDir)
{
// Sadly, mono implemention of System.IO.Compression is really poor (as of 2015/Aug), causing OOM when parallel zipping a large set of files.
// However, Ionic is MUCH slower than .NET's native implementation (2x+ slower in our build farm), so we stick to the faster solution on PC.
// The code duplication in the threadprocs is unfortunate here, and hopefully we can settle on .NET's implementation on both platforms eventually.
if (Utils.IsRunningOnMono)
{
Parallel.ForEach(ZipFiles,
(ZipFile) =>
{
using (var ZipArchive = Ionic.Zip.ZipFile.Read(ZipFile.FullName))
{
ZipArchive.ExtractAll(RootDir.FullName, Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);
}
});
}
else
{
Parallel.ForEach(ZipFiles,
(ZipFile) =>
{
// unzip the files manually instead of caling ZipFile.ExtractToDirectory() because we need to overwrite readonly files. Because of this, creating the directories is up to us as well.
using (var ZipArchive = System.IO.Compression.ZipFile.OpenRead(ZipFile.FullName))
{
foreach (var Entry in ZipArchive.Entries)
{
// Use CommandUtils.CombinePaths to ensure directory separators get converted correctly. On mono on *nix, if the path has backslashes it will not convert it.
var ExtractedFilename = CommandUtils.CombinePaths(RootDir.FullName, Entry.FullName);
// Zips can contain empty dirs. Ours usually don't have them, but we should support it.
if (Path.GetFileName(ExtractedFilename).Length == 0)
{
Directory.CreateDirectory(ExtractedFilename);
}
else
{
// We must delete any existing file, even if it's readonly. .Net does not do this by default.
if (File.Exists(ExtractedFilename))
{
InternalUtils.SafeDeleteFile(ExtractedFilename, true);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(ExtractedFilename));
}
Entry.ExtractToFile(ExtractedFilename, true);
}
}
}
});
}
}
/// <summary>
/// Gets the directory used to store data for the given node
/// </summary>
/// <param name="BaseDir">A local or shared temp storage root directory.</param>
/// <param name="NodeName">Name of the node</param>
/// <returns>Directory to contain a node's data</returns>
static DirectoryReference GetDirectoryForNode(DirectoryReference BaseDir, string NodeName)
{
return DirectoryReference.Combine(BaseDir, NodeName);
}
/// <summary>
/// Gets the path to the manifest created for a node's output.
/// </summary>
/// <param name="BaseDir">A local or shared temp storage root directory.</param>
/// <param name="NodeName">Name of the node to get the file for</param>
/// <param name="BlockName">Name of the output block to get the manifest for</param>
static FileReference GetManifestLocation(DirectoryReference BaseDir, string NodeName, string BlockName)
{
return FileReference.Combine(BaseDir, NodeName, String.IsNullOrEmpty(BlockName)? "Manifest.xml" : String.Format("Manifest-{0}.xml", BlockName));
}
/// <summary>
/// Gets the path to the file created to store a tag manifest for a node
/// </summary>
/// <param name="BaseDir">A local or shared temp storage root directory.</param>
/// <param name="NodeName">Name of the node to get the file for</param>
/// <param name="TagName">Name of the tag to get the manifest for</param>
static FileReference GetTaggedFileListLocation(DirectoryReference BaseDir, string NodeName, string TagName)
{
Debug.Assert(TagName.StartsWith("#"));
return FileReference.Combine(BaseDir, NodeName, String.Format("Tag-{0}.xml", TagName.Substring(1)));
}
/// <summary>
/// Gets the path to a file created to indicate that a node is complete, under the given base directory.
/// </summary>
/// <param name="BaseDir">A local or shared temp storage root directory.</param>
/// <param name="NodeName">Name of the node to get the file for</param>
static FileReference GetCompleteMarkerFile(DirectoryReference BaseDir, string NodeName)
{
return FileReference.Combine(GetDirectoryForNode(BaseDir, NodeName), "Complete");
}
}
/// <summary>
/// Automated tests for temp storage
/// </summary>
class TempStorageTests : BuildCommand
{
/// <summary>
/// Run the automated tests
/// </summary>
public override void ExecuteBuild()
{
// Get all the shared directories
DirectoryReference RootDir = new DirectoryReference(CommandUtils.CmdEnv.LocalRoot);
DirectoryReference LocalDir = DirectoryReference.Combine(RootDir, "Engine", "Saved", "TestTempStorage-Local");
CommandUtils.CreateDirectory_NoExceptions(LocalDir.FullName);
CommandUtils.DeleteDirectoryContents(LocalDir.FullName);
DirectoryReference SharedDir = DirectoryReference.Combine(RootDir, "Engine", "Saved", "TestTempStorage-Shared");
CommandUtils.CreateDirectory_NoExceptions(SharedDir.FullName);
CommandUtils.DeleteDirectoryContents(SharedDir.FullName);
DirectoryReference WorkingDir = DirectoryReference.Combine(RootDir, "Engine", "Saved", "TestTempStorage-Working");
CommandUtils.CreateDirectory_NoExceptions(WorkingDir.FullName);
CommandUtils.DeleteDirectoryContents(WorkingDir.FullName);
// Create the temp storage object
TempStorage TempStore = new TempStorage(WorkingDir, LocalDir, SharedDir, true);
// Create a working directory, and copy some source files into it
DirectoryReference SourceDir = DirectoryReference.Combine(RootDir, "Engine", "Source", "Runtime");
if(!CommandUtils.CopyDirectory_NoExceptions(SourceDir.FullName, WorkingDir.FullName, true))
{
throw new AutomationException("Couldn't copy {0} to {1}", SourceDir.FullName, WorkingDir.FullName);
}
// Save the default output
Dictionary<FileReference, DateTime> DefaultOutput = SelectFiles(WorkingDir, 'a', 'f');
TempStore.Archive("TestNode", null, DefaultOutput.Keys.ToArray(), false);
Dictionary<FileReference, DateTime> NamedOutput = SelectFiles(WorkingDir, 'g', 'i');
TempStore.Archive("TestNode", "NamedOutput", NamedOutput.Keys.ToArray(), true);
// Check both outputs are still ok
TempStorageManifest DefaultManifest = TempStore.Retreive("TestNode", null);
CheckManifest(WorkingDir, DefaultManifest, DefaultOutput);
TempStorageManifest NamedManifest = TempStore.Retreive("TestNode", "NamedOutput");
CheckManifest(WorkingDir, NamedManifest, NamedOutput);
// Delete local temp storage and the working directory and try again
CommandUtils.Log("Clearing local folders...");
CommandUtils.DeleteDirectoryContents(WorkingDir.FullName);
CommandUtils.DeleteDirectoryContents(LocalDir.FullName);
// First output should fail
CommandUtils.Log("Checking default manifest is now unavailable...");
bool bGotManifest = false;
try
{
TempStore.Retreive("TestNode", null);
bGotManifest = true;
}
catch
{
bGotManifest = false;
}
if(bGotManifest)
{
throw new AutomationException("Did not expect shared temp storage manifest to exist");
}
// Second one should be fine
TempStorageManifest NamedManifestFromShared = TempStore.Retreive("TestNode", "NamedOutput");
CheckManifest(WorkingDir, NamedManifestFromShared, NamedOutput);
}
/// <summary>
/// Enumerate all the files beginning with a letter within a certain range
/// </summary>
/// <param name="SourceDir">The directory to read from</param>
/// <param name="CharRangeBegin">First character in the range to files to return</param>
/// <param name="CharRangeEnd">Last character (inclusive) in the range of files to return</param>
/// <returns>Mapping from filename to timestamp</returns>
static Dictionary<FileReference, DateTime> SelectFiles(DirectoryReference SourceDir, char CharRangeBegin, char CharRangeEnd)
{
Dictionary<FileReference, DateTime> ArchiveFileToTime = new Dictionary<FileReference,DateTime>();
foreach(FileInfo FileInfo in new DirectoryInfo(SourceDir.FullName).EnumerateFiles("*", SearchOption.AllDirectories))
{
char FirstCharacter = Char.ToLower(FileInfo.Name[0]);
if(FirstCharacter >= CharRangeBegin && FirstCharacter <= CharRangeEnd)
{
ArchiveFileToTime.Add(new FileReference(FileInfo), FileInfo.LastWriteTimeUtc);
}
}
return ArchiveFileToTime;
}
/// <summary>
/// Checks that a manifest matches the files on disk
/// </summary>
/// <param name="RootDir">Root directory for relative paths in the manifest</param>
/// <param name="Manifest">Manifest to check</param>
/// <param name="Files">Mapping of filename to timestamp as expected in the manifest</param>
static void CheckManifest(DirectoryReference RootDir, TempStorageManifest Manifest, Dictionary<FileReference, DateTime> Files)
{
if(Files.Count != Manifest.Files.Length)
{
throw new AutomationException("Number of files in manifest does not match");
}
foreach(TempStorageFile ManifestFile in Manifest.Files)
{
FileReference File = ManifestFile.ToFileReference(RootDir);
if(!FileReference.Exists(File))
{
throw new AutomationException("File in manifest does not exist");
}
DateTime OriginalTime;
if(!Files.TryGetValue(File, out OriginalTime))
{
throw new AutomationException("File in manifest did not exist previously");
}
double DiffSeconds = (new FileInfo(File.FullName).LastWriteTimeUtc - OriginalTime).TotalSeconds;
if(Math.Abs(DiffSeconds) > 2)
{
throw new AutomationException("Incorrect timestamp for {0}", ManifestFile.RelativePath);
}
}
}
}
/// <summary>
/// Commandlet to clean up all folders under a temp storage root that are older than a given number of days
/// </summary>
[Help("Removes folders in a given temp storage directory that are older than a certain time.")]
[Help("TempStorageDir=<Directory>", "Path to the root temp storage directory")]
[Help("Days=<N>", "Number of days to keep in temp storage")]
class CleanTempStorage : BuildCommand
{
/// <summary>
/// Entry point for the commandlet
/// </summary>
public override void ExecuteBuild()
{
string TempStorageDir = ParseParamValue("TempStorageDir", null);
if (TempStorageDir == null)
{
throw new AutomationException("Missing -TempStorageDir parameter");
}
string Days = ParseParamValue("Days", null);
if (Days == null)
{
throw new AutomationException("Missing -Days parameter");
}
double DaysValue;
if (!Double.TryParse(Days, out DaysValue))
{
throw new AutomationException("'{0}' is not a valid value for the -Days parameter", Days);
}
DateTime RetainTime = DateTime.UtcNow - TimeSpan.FromDays(DaysValue);
// Enumerate all the build directories
CommandUtils.Log("Scanning {0}...", TempStorageDir);
int NumBuilds = 0;
List<DirectoryInfo> BuildsToDelete = new List<DirectoryInfo>();
foreach (DirectoryInfo StreamDirectory in new DirectoryInfo(TempStorageDir).EnumerateDirectories().OrderBy(x => x.Name))
{
CommandUtils.Log("Scanning {0}...", StreamDirectory.FullName);
foreach (DirectoryInfo BuildDirectory in StreamDirectory.EnumerateDirectories())
{
if(!BuildDirectory.EnumerateFiles("*", SearchOption.AllDirectories).Any(x => x.LastWriteTimeUtc > RetainTime))
{
BuildsToDelete.Add(BuildDirectory);
}
NumBuilds++;
}
}
CommandUtils.Log("Found {0} builds; {1} to delete.", NumBuilds, BuildsToDelete.Count);
// Loop through them all, checking for files older than the delete time
for (int Idx = 0; Idx < BuildsToDelete.Count; Idx++)
{
try
{
CommandUtils.Log("[{0}/{1}] Deleting {2}...", Idx + 1, BuildsToDelete.Count, BuildsToDelete[Idx].FullName);
BuildsToDelete[Idx].Delete(true);
}
catch (Exception Ex)
{
CommandUtils.LogWarning("Failed to delete old manifest folder; will try one file at a time: {0}", Ex);
CommandUtils.DeleteDirectory_NoExceptions(true, BuildsToDelete[Idx].FullName);
}
}
// Try to delete any empty branch folders
foreach (DirectoryInfo StreamDirectory in new DirectoryInfo(TempStorageDir).EnumerateDirectories())
{
if(StreamDirectory.EnumerateDirectories().Count() == 0 && StreamDirectory.EnumerateFiles().Count() == 0)
{
try
{
StreamDirectory.Delete();
}
catch (IOException)
{
// only catch "directory is not empty type exceptions, if possible. Best we can do is check for IOException.
}
catch (Exception Ex)
{
CommandUtils.LogWarning("Unexpected failure trying to delete (potentially empty) stream directory {0}: {1}", StreamDirectory.FullName, Ex);
}
}
}
}
}
}