You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rnx #rb none #jira none #ROBOMERGE-OWNER: ryan.durand #ROBOMERGE-AUTHOR: ryan.durand #ROBOMERGE-SOURCE: CL 10869242 in //Fortnite/Release-12.00/... via CL 10869536 #ROBOMERGE-BOT: FORTNITE (Main -> Dev-EngineMerge) (v613-10869866) [CL 10870955 by Ryan Durand in Main branch]
1553 lines
54 KiB
C#
1553 lines
54 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Tools.DotNETCommon;
|
|
using Tools.DotNETCommon.Perforce;
|
|
|
|
namespace BuildAgent.Workspace.Common
|
|
{
|
|
/// <summary>
|
|
/// Represents a repository of streams and cached data
|
|
/// </summary>
|
|
class Repository
|
|
{
|
|
/// <summary>
|
|
/// The current transaction state. Used to determine whether a repository needs to be cleaned on startup.
|
|
/// </summary>
|
|
enum RepositoryState
|
|
{
|
|
Dirty,
|
|
Clean,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The file signature and version. Update this to introduce breaking changes and ignore old repositories.
|
|
/// </summary>
|
|
const int CurrentSignature = ('W' << 24) | ('T' << 16) | 1;
|
|
|
|
/// <summary>
|
|
/// The current revision number.
|
|
/// </summary>
|
|
const int CurrentRevision = 1;
|
|
|
|
/// <summary>
|
|
/// Constant for syncing the latest change number
|
|
/// </summary>
|
|
public const int LatestChangeNumber = -1;
|
|
|
|
/// <summary>
|
|
/// Name of the signature file for a repository. This
|
|
/// </summary>
|
|
const string SignatureFileName = "Repository.sig";
|
|
|
|
/// <summary>
|
|
/// Name of the main data file for a repository
|
|
/// </summary>
|
|
const string DataFileName = "Repository.dat";
|
|
|
|
/// <summary>
|
|
/// Name of the server and port for
|
|
/// </summary>
|
|
string ServerAndPort;
|
|
|
|
/// <summary>
|
|
/// Name of the Perforce user
|
|
/// </summary>
|
|
string UserName;
|
|
|
|
/// <summary>
|
|
/// Name of the host
|
|
/// </summary>
|
|
string HostName;
|
|
|
|
/// <summary>
|
|
/// Incrementing number assigned to sequential operations that modify files. Used to age out files in the cache.
|
|
/// </summary>
|
|
uint NextSequenceNumber;
|
|
|
|
/// <summary>
|
|
/// Whether a repair operation should be run on this workspace. Set whenever the state may be inconsistent.
|
|
/// </summary>
|
|
bool bRequiresRepair;
|
|
|
|
/// <summary>
|
|
/// Connection to the Perforce server
|
|
/// </summary>
|
|
PerforceConnection Perforce;
|
|
|
|
/// <summary>
|
|
/// The root directory for the stash
|
|
/// </summary>
|
|
readonly DirectoryReference BaseDir;
|
|
|
|
/// <summary>
|
|
/// Root directory for storing cache files
|
|
/// </summary>
|
|
readonly DirectoryReference CacheDir;
|
|
|
|
/// <summary>
|
|
/// Root directory for storing workspace files
|
|
/// </summary>
|
|
readonly DirectoryReference WorkspaceDir;
|
|
|
|
/// <summary>
|
|
/// Set of clients that we're created. Used to avoid updating multiple times during one run.
|
|
/// </summary>
|
|
HashSet<string> ClientNames = new HashSet<string>();
|
|
|
|
/// <summary>
|
|
/// Set of unique cache entries. We use this to ensure new names in the cache are unique.
|
|
/// </summary>
|
|
HashSet<ulong> CacheEntries = new HashSet<ulong>();
|
|
|
|
/// <summary>
|
|
/// List of all the staged files
|
|
/// </summary>
|
|
WorkspaceDirectoryInfo Workspace;
|
|
|
|
/// <summary>
|
|
/// All the files which are currently being tracked
|
|
/// </summary>
|
|
Dictionary<FileContentId, TrackedFileInfo> ContentIdToTrackedFile = new Dictionary<FileContentId, TrackedFileInfo>();
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="ServerAndPort">Perforce server and port to connect to</param>
|
|
/// <param name="UserName">Name of the user to login to Perforce with</param>
|
|
/// <param name="HostName">Name of the current host</param>
|
|
/// <param name="NextSequenceNumber">The next sequence number for operations</param>
|
|
/// <param name="BaseDir">The root directory for the stash</param>
|
|
private Repository(string ServerAndPort, string UserName, string HostName, uint NextSequenceNumber, DirectoryReference BaseDir)
|
|
{
|
|
// Make sure all the fields are valid
|
|
if(UserName == null || HostName == null)
|
|
{
|
|
PerforceConnection Perforce = CreatePerforceConnection(ServerAndPort, UserName, null);
|
|
|
|
InfoRecord Info = Perforce.Info(InfoOptions.ShortOutput).Data;
|
|
if(UserName == null)
|
|
{
|
|
UserName = Info.UserName;
|
|
}
|
|
if(HostName == null)
|
|
{
|
|
HostName = Info.ClientHost;
|
|
}
|
|
}
|
|
|
|
// Save the Perforce settings
|
|
this.ServerAndPort = ServerAndPort;
|
|
this.UserName = UserName;
|
|
this.HostName = HostName;
|
|
this.NextSequenceNumber = NextSequenceNumber;
|
|
|
|
// Create the perforce connection
|
|
Perforce = CreatePerforceConnection(ServerAndPort, UserName, null);
|
|
|
|
// Get all the directories
|
|
this.BaseDir = BaseDir;
|
|
DirectoryReference.CreateDirectory(BaseDir);
|
|
|
|
this.CacheDir = DirectoryReference.Combine(BaseDir, "Cache");
|
|
DirectoryReference.CreateDirectory(CacheDir);
|
|
|
|
this.WorkspaceDir = DirectoryReference.Combine(BaseDir, "Sync");
|
|
DirectoryReference.CreateDirectory(WorkspaceDir);
|
|
|
|
// Create the workspace
|
|
this.Workspace = new WorkspaceDirectoryInfo(WorkspaceDir);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a repository at the given location
|
|
/// </summary>
|
|
/// <param name="ServerAndPort">The Perforce server and port</param>
|
|
/// <param name="UserName">The Perforce username to connect with</param>
|
|
/// <param name="BaseDir">The base directory for the repository</param>
|
|
/// <returns>New repository instance</returns>
|
|
public static Repository Create(string ServerAndPort, string UserName, DirectoryReference BaseDir)
|
|
{
|
|
Log.TraceInformation("Creating repository at {0}...", BaseDir);
|
|
|
|
DirectoryReference.CreateDirectory(BaseDir);
|
|
FileUtils.ForceDeleteDirectoryContents(BaseDir);
|
|
|
|
Repository Repo = new Repository(ServerAndPort, UserName, null, 1, BaseDir);
|
|
Repo.Save(RepositoryState.Clean);
|
|
Repo.CreateCacheHierarchy();
|
|
|
|
FileReference SignatureFile = FileReference.Combine(BaseDir, SignatureFileName);
|
|
using(BinaryWriter Writer = new BinaryWriter(File.Open(SignatureFile.FullName, FileMode.Create, FileAccess.Write, FileShare.Read)))
|
|
{
|
|
Writer.Write(CurrentSignature);
|
|
}
|
|
|
|
return Repo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests whether a repository exists in the given directory
|
|
/// </summary>
|
|
/// <param name="BaseDir"></param>
|
|
/// <returns></returns>
|
|
public static bool Exists(DirectoryReference BaseDir)
|
|
{
|
|
FileReference SignatureFile = FileReference.Combine(BaseDir, SignatureFileName);
|
|
if(FileReference.Exists(SignatureFile))
|
|
{
|
|
using(BinaryReader Reader = new BinaryReader(File.Open(SignatureFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)))
|
|
{
|
|
int Signature = Reader.ReadInt32();
|
|
if(Signature == CurrentSignature)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a repository from disk
|
|
/// </summary>
|
|
/// <param name="ServerAndPort">The Perforce server and port</param>
|
|
/// <param name="UserName">The Perforce username to connect with</param>
|
|
/// <param name="BaseDir">The base directory for the repository</param>
|
|
public static Repository Load(string ServerAndPort, string UserName, DirectoryReference BaseDir)
|
|
{
|
|
if(!Exists(BaseDir))
|
|
{
|
|
throw new FatalErrorException("No valid repository found at {0}", BaseDir);
|
|
}
|
|
|
|
FileReference DataFile = FileReference.Combine(BaseDir, DataFileName);
|
|
RestoreBackup(DataFile);
|
|
|
|
Repository Repo;
|
|
using(BinaryReader Reader = new BinaryReader(File.Open(DataFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)))
|
|
{
|
|
int Revision = Reader.ReadInt32();
|
|
if(Revision != CurrentRevision)
|
|
{
|
|
throw new FatalErrorException("Unsupported data format (revision {0}, expected {1})", Revision, CurrentRevision);
|
|
}
|
|
|
|
bool bRequiresRepair = Reader.ReadBoolean();
|
|
Reader.ReadString(); // Originally client name; now ignored
|
|
uint NextSequenceNumber = Reader.ReadUInt32();
|
|
|
|
Repo = new Repository(ServerAndPort, UserName, null, NextSequenceNumber, BaseDir);
|
|
Repo.bRequiresRepair = bRequiresRepair;
|
|
|
|
int NumTrackedFiles = Reader.ReadInt32();
|
|
for(int Idx = 0; Idx < NumTrackedFiles; Idx++)
|
|
{
|
|
TrackedFileInfo TrackedFile = new TrackedFileInfo(Repo.CacheDir, Reader);
|
|
Repo.ContentIdToTrackedFile.Add(TrackedFile.ContentId, TrackedFile);
|
|
Repo.CacheEntries.Add(TrackedFile.CacheId);
|
|
}
|
|
|
|
Repo.Workspace.Read(Reader);
|
|
}
|
|
|
|
Repo.RunOptionalRepair();
|
|
return Repo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save the state of the repository
|
|
/// </summary>
|
|
private void Save(RepositoryState State)
|
|
{
|
|
FileReference DataFile = FileReference.Combine(BaseDir, DataFileName);
|
|
|
|
BeginTransaction(DataFile);
|
|
using(BinaryWriter Writer = new BinaryWriter(File.Open(DataFile.FullName, FileMode.Create, FileAccess.Write, FileShare.Read)))
|
|
{
|
|
Writer.Write(CurrentRevision);
|
|
Writer.Write(bRequiresRepair || (State != RepositoryState.Clean));
|
|
Writer.Write(""); // Originally client name; now ignored
|
|
Writer.Write(NextSequenceNumber);
|
|
|
|
Writer.Write(ContentIdToTrackedFile.Count);
|
|
foreach(TrackedFileInfo TrackedFile in ContentIdToTrackedFile.Values)
|
|
{
|
|
TrackedFile.Write(Writer);
|
|
}
|
|
|
|
Workspace.Write(Writer);
|
|
}
|
|
CompleteTransaction(DataFile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Perforce connection for the given settings
|
|
/// </summary>
|
|
/// <param name="ServerAndPort">The name and port for the server</param>
|
|
/// <param name="UserName">The username to connect to the server with</param>
|
|
/// <param name="ClientName">Name of the client to use when syncing files</param>
|
|
/// <returns>New Perforce connection instance</returns>
|
|
static PerforceConnection CreatePerforceConnection(string ServerAndPort, string UserName, string ClientName)
|
|
{
|
|
// Create the Perforce connection
|
|
List<string> GlobalOptions = new List<string>();
|
|
GlobalOptions.Add("-zprog=WorkspaceTool");
|
|
GlobalOptions.Add(String.Format("-zversion={0}", Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
|
if(ServerAndPort != null)
|
|
{
|
|
GlobalOptions.Add(String.Format("-p {0}", ServerAndPort));
|
|
}
|
|
if(UserName != null)
|
|
{
|
|
GlobalOptions.Add(String.Format("-u {0}", UserName));
|
|
}
|
|
if(ClientName != null)
|
|
{
|
|
GlobalOptions.Add(String.Format("-c {0}", ClientName));
|
|
}
|
|
return new PerforceConnection(String.Join(" ", GlobalOptions));
|
|
}
|
|
|
|
#region Commands
|
|
|
|
/// <summary>
|
|
/// Cleans the current workspace
|
|
/// </summary>
|
|
public void Clean()
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Log.TraceInformation("Cleaning workspace...");
|
|
using(LogIndentScope Indent = new LogIndentScope(" "))
|
|
{
|
|
CleanInternal();
|
|
}
|
|
|
|
Log.TraceInformation("Completed in {0:0.0}s", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans the current workspace
|
|
/// </summary>
|
|
private void CleanInternal()
|
|
{
|
|
FileInfo[] FilesToDelete;
|
|
DirectoryInfo[] DirectoriesToDelete;
|
|
using(LogStatusScope Status = new LogStatusScope("Finding files to clean..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Workspace.Refresh(out FilesToDelete, out DirectoriesToDelete);
|
|
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
if(FilesToDelete.Length > 0 || DirectoriesToDelete.Length > 0)
|
|
{
|
|
List<string> Paths = new List<string>();
|
|
Paths.AddRange(DirectoriesToDelete.Select(x => String.Format("/{0}/...", new DirectoryReference(x).MakeRelativeTo(WorkspaceDir).Replace(Path.DirectorySeparatorChar, '/'))));
|
|
Paths.AddRange(FilesToDelete.Select(x => String.Format("/{0}", new FileReference(x).MakeRelativeTo(WorkspaceDir).Replace(Path.DirectorySeparatorChar, '/'))));
|
|
|
|
foreach(string Path in Paths.OrderBy(x => x))
|
|
{
|
|
Log.TraceInformation(" {0}", Path);
|
|
}
|
|
|
|
using(LogStatusScope Scope = new LogStatusScope("Cleaning files..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
using(ThreadPoolWorkQueue Queue = new ThreadPoolWorkQueue())
|
|
{
|
|
foreach(FileInfo FileToDelete in FilesToDelete)
|
|
{
|
|
Queue.Enqueue(() => FileUtils.ForceDeleteFile(FileToDelete));
|
|
}
|
|
foreach(DirectoryInfo DirectoryToDelete in DirectoriesToDelete)
|
|
{
|
|
Queue.Enqueue(() => FileUtils.ForceDeleteDirectory(DirectoryToDelete));
|
|
}
|
|
}
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
Save(RepositoryState.Clean);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Empties the staging directory of any staged files
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Log.TraceInformation("Clearing workspace...");
|
|
using(LogIndentScope Indent = new LogIndentScope(" "))
|
|
{
|
|
CleanInternal();
|
|
RemoveFilesFromWorkspace(new StreamDirectoryInfo());
|
|
Save(RepositoryState.Clean);
|
|
}
|
|
|
|
Log.TraceInformation("Completed in {0:0.0}s", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dumps the contents of the repository to the log for analysis
|
|
/// </summary>
|
|
public void Dump()
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Log.TraceInformation("Dumping repository to log...");
|
|
|
|
WorkspaceFileInfo[] WorkspaceFiles = Workspace.GetFiles().OrderBy(x => x.GetLocation().FullName).ToArray();
|
|
if(WorkspaceFiles.Length > 0)
|
|
{
|
|
Log.TraceLog(" Workspace:");
|
|
foreach(WorkspaceFileInfo File in WorkspaceFiles)
|
|
{
|
|
Log.TraceLog(" {0,-128} [{1,-48}] [{2,20:n0}] [{3,20}]{4}", File.GetClientPath(), File.ContentId, File.Length, File.LastModifiedTicks, File.bReadOnly? "" : " [ writable ]");
|
|
}
|
|
}
|
|
|
|
if(ContentIdToTrackedFile.Count > 0)
|
|
{
|
|
Log.TraceLog(" Cache:");
|
|
foreach(KeyValuePair<FileContentId, TrackedFileInfo> Pair in ContentIdToTrackedFile)
|
|
{
|
|
Log.TraceLog(" {0,-128} [{1,-48}] [{2,20:n0}] [{3,20}]{4}", Pair.Value.GetLocation(), Pair.Key, Pair.Value.Length, Pair.Value.LastModifiedTicks, Pair.Value.bReadOnly? "" : "[ writable ]");
|
|
}
|
|
}
|
|
|
|
Log.TraceInformation("Completed in {0:0.0}s", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the integrity of the cache
|
|
/// </summary>
|
|
public void Repair()
|
|
{
|
|
using(LogStatusScope Status = new LogStatusScope("Checking cache..."))
|
|
{
|
|
// Make sure all the folders exist in the cache
|
|
CreateCacheHierarchy();
|
|
|
|
// Check that all the files in the cache appear as we expect them to
|
|
List<TrackedFileInfo> TrackedFiles = ContentIdToTrackedFile.Values.ToList();
|
|
foreach(TrackedFileInfo TrackedFile in TrackedFiles)
|
|
{
|
|
if(!TrackedFile.CheckIntegrity())
|
|
{
|
|
RemoveTrackedFile(TrackedFile);
|
|
}
|
|
}
|
|
|
|
// Clear the repair flag
|
|
bRequiresRepair = false;
|
|
|
|
Save(RepositoryState.Clean);
|
|
|
|
Status.SetProgress("Done");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans the current workspace
|
|
/// </summary>
|
|
public void Revert(string ClientName)
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
PerforceConnection Client = CreatePerforceConnection(ServerAndPort, UserName, ClientName);
|
|
RevertInternal(Client, ClientName);
|
|
|
|
Log.TraceInformation("Completed in {0:0.0}s", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the bRequiresRepair flag, and repairs/resets it if set.
|
|
/// </summary>
|
|
private void RunOptionalRepair()
|
|
{
|
|
if(bRequiresRepair)
|
|
{
|
|
Repair();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shrink the size of the cache to the given size
|
|
/// </summary>
|
|
/// <param name="MaxSize">The maximum cache size, in bytes</param>
|
|
public void Purge(long MaxSize)
|
|
{
|
|
Log.TraceInformation("Purging cache (limit {0:n0} bytes)...", MaxSize);
|
|
using(new LogIndentScope(" "))
|
|
{
|
|
List<TrackedFileInfo> CachedFiles = ContentIdToTrackedFile.Values.OrderBy(x => x.SequenceNumber).ToList();
|
|
|
|
int NumRemovedFiles = 0;
|
|
long TotalSize = CachedFiles.Sum(x => x.Length);
|
|
|
|
while(MaxSize < TotalSize && NumRemovedFiles < CachedFiles.Count)
|
|
{
|
|
TrackedFileInfo File = CachedFiles[NumRemovedFiles];
|
|
|
|
RemoveTrackedFile(File);
|
|
TotalSize -= File.Length;
|
|
|
|
NumRemovedFiles++;
|
|
}
|
|
|
|
Save(RepositoryState.Clean);
|
|
|
|
Log.TraceInformation("{0} files removed, {1} files remaining, new size {2:n0} bytes.", NumRemovedFiles, CachedFiles.Count - NumRemovedFiles, TotalSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures the client for the given stream
|
|
/// </summary>
|
|
/// <param name="ClientName">Name of the client</param>
|
|
/// <param name="StreamName">Name of the stream</param>
|
|
public void Setup(string ClientName, string StreamName)
|
|
{
|
|
UpdateClient(ClientName, StreamName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prints stats showing coherence between different streams
|
|
/// </summary>
|
|
public void Stats(string ClientName, List<string> StreamNames, List<string> Filters)
|
|
{
|
|
Log.TraceInformation("Finding stats for {0} streams", StreamNames.Count);
|
|
using (new LogIndentScope(" "))
|
|
{
|
|
// Update the list of files in each stream
|
|
Tuple<int, StreamDirectoryInfo>[] StreamState = new Tuple<int, StreamDirectoryInfo>[StreamNames.Count];
|
|
for (int Idx = 0; Idx < StreamNames.Count; Idx++)
|
|
{
|
|
string StreamName = StreamNames[Idx];
|
|
Log.TraceInformation("Finding contents of {0}:", StreamName);
|
|
|
|
using (new LogIndentScope(" "))
|
|
{
|
|
ClientNames.Remove(ClientName); // Force the client to be updated
|
|
|
|
PerforceConnection Client = UpdateClient(ClientName, StreamName);
|
|
|
|
int ChangeNumber = GetLatestClientChange(Client, ClientName);
|
|
Log.TraceInformation("Latest change is CL {0}", ChangeNumber);
|
|
|
|
RevertInternal(Client, ClientName);
|
|
ClearClientHaveTable(Client, ClientName);
|
|
UpdateClientHaveTable(Client, ClientName, ChangeNumber, Filters);
|
|
|
|
StreamDirectoryInfo Contents = FindClientContents(Client, ClientName, ChangeNumber, false);
|
|
StreamState[Idx] = Tuple.Create(ChangeNumber, Contents);
|
|
|
|
GC.Collect();
|
|
}
|
|
}
|
|
|
|
// Find stats for
|
|
using (LogStatusScope Scope = new LogStatusScope("Finding usage stats..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Find the set of files in each stream
|
|
HashSet<FileContentId>[] FilesInStream = new HashSet<FileContentId>[StreamNames.Count];
|
|
for (int Idx = 0; Idx < StreamNames.Count; Idx++)
|
|
{
|
|
List<StreamFileInfo> Files = StreamState[Idx].Item2.GetFiles();
|
|
FilesInStream[Idx] = new HashSet<FileContentId>(Files.Select(x => x.ContentId));
|
|
}
|
|
|
|
// Build a table showing amount of unique content in each stream
|
|
string[,] Cells = new string[StreamNames.Count + 1, StreamNames.Count + 1];
|
|
Cells[0, 0] = "";
|
|
for (int Idx = 0; Idx < StreamNames.Count; Idx++)
|
|
{
|
|
Cells[Idx + 1, 0] = StreamNames[Idx];
|
|
Cells[0, Idx + 1] = StreamNames[Idx];
|
|
}
|
|
|
|
// Populate the table
|
|
for (int RowIdx = 0; RowIdx < StreamNames.Count; RowIdx++)
|
|
{
|
|
List<StreamFileInfo> Files = StreamState[RowIdx].Item2.GetFiles();
|
|
for (int ColIdx = 0; ColIdx < StreamNames.Count; ColIdx++)
|
|
{
|
|
long DiffSize = Files.Where(x => !FilesInStream[ColIdx].Contains(x.ContentId)).Sum(x => x.Length);
|
|
Cells[RowIdx + 1, ColIdx + 1] = String.Format("{0:0.0}mb", DiffSize / (1024.0 * 1024.0));
|
|
}
|
|
}
|
|
|
|
// Find the width of each row
|
|
int[] ColWidths = new int[StreamNames.Count + 1];
|
|
for (int ColIdx = 0; ColIdx < StreamNames.Count + 1; ColIdx++)
|
|
{
|
|
for (int RowIdx = 0; RowIdx < StreamNames.Count + 1; RowIdx++)
|
|
{
|
|
ColWidths[ColIdx] = Math.Max(ColWidths[ColIdx], Cells[RowIdx, ColIdx].Length);
|
|
}
|
|
}
|
|
|
|
// Print the table
|
|
Log.TraceInformation("");
|
|
Log.TraceInformation("Each row shows the size of files in a stream which are unique to that stream compared to each column:");
|
|
Log.TraceInformation("");
|
|
for (int RowIdx = 0; RowIdx < StreamNames.Count + 1; RowIdx++)
|
|
{
|
|
StringBuilder Row = new StringBuilder();
|
|
for (int ColIdx = 0; ColIdx < StreamNames.Count + 1; ColIdx++)
|
|
{
|
|
string Cell = Cells[RowIdx, ColIdx];
|
|
Row.Append(' ', ColWidths[ColIdx] - Cell.Length);
|
|
Row.Append(Cell);
|
|
Row.Append(" | ");
|
|
}
|
|
Log.TraceInformation("{0}", Row.ToString());
|
|
}
|
|
Log.TraceInformation("");
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prints information about the repository state
|
|
/// </summary>
|
|
public void Status()
|
|
{
|
|
// Print size stats
|
|
Log.TraceInformation("Cache contains {0:n0} files, {1:n1}mb", ContentIdToTrackedFile.Count, ContentIdToTrackedFile.Values.Sum(x => x.Length) / (1024.0 * 1024.0));
|
|
Log.TraceInformation("Stage contains {0:n0} files, {1:n1}mb", Workspace.GetFiles().Count, Workspace.GetFiles().Sum(x => x.Length) / (1024.0 * 1024.0));
|
|
|
|
// Print the contents of the workspace
|
|
string[] Differences = Workspace.FindDifferences();
|
|
if(Differences.Length > 0)
|
|
{
|
|
Log.TraceInformation("Local changes:");
|
|
foreach(string Difference in Differences)
|
|
{
|
|
if(Difference.StartsWith("+"))
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Green;
|
|
}
|
|
else if(Difference.StartsWith("-"))
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Red;
|
|
}
|
|
else if(Difference.StartsWith("!"))
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
}
|
|
else
|
|
{
|
|
Console.ResetColor();
|
|
}
|
|
Log.TraceInformation(" {0}", Difference);
|
|
}
|
|
Console.ResetColor();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switches to the given stream
|
|
/// </summary>
|
|
/// <param name="ClientName">Name of the client to sync</param>
|
|
/// <param name="StreamName">Name of the stream to sync</param>
|
|
/// <param name="ChangeNumber">Changelist number to sync. -1 to sync to latest.</param>
|
|
/// <param name="Filters">List of filters to be applied to the workspace</param>
|
|
/// <param name="bFakeSync">Whether to simulate the syncing operation rather than actually getting files from the server</param>
|
|
/// <param name="CacheFile">If set, uses the given file to cache the contents of the workspace. This can improve sync times when multiple machines sync the same workspace.</param>
|
|
public void Sync(string ClientName, string StreamName, int ChangeNumber, List<string> Filters, bool bFakeSync, FileReference CacheFile)
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
if(ChangeNumber == -1)
|
|
{
|
|
Log.TraceInformation("Syncing to {0} at latest", StreamName);
|
|
}
|
|
else
|
|
{
|
|
Log.TraceInformation("Syncing to {0} at CL {1}", StreamName, ChangeNumber);
|
|
}
|
|
|
|
using(LogIndentScope Indent = new LogIndentScope(" "))
|
|
{
|
|
// Update the client to the current stream
|
|
PerforceConnection Client = UpdateClient(ClientName, StreamName);
|
|
|
|
// Get the latest change number
|
|
if(ChangeNumber == -1)
|
|
{
|
|
ChangeNumber = GetLatestClientChange(Client, ClientName);
|
|
}
|
|
|
|
// Revert any open files
|
|
RevertInternal(Client, ClientName);
|
|
|
|
// Force the P4 metadata to match up
|
|
UpdateClientHaveTable(Client, ClientName, ChangeNumber, Filters);
|
|
|
|
// Clean the current workspace
|
|
CleanInternal();
|
|
|
|
// Update the state of the current stream, if necessary
|
|
StreamDirectoryInfo Contents;
|
|
if(CacheFile == null)
|
|
{
|
|
Contents = FindClientContents(Client, ClientName, ChangeNumber, bFakeSync);
|
|
}
|
|
else
|
|
{
|
|
if(FileReference.Exists(CacheFile))
|
|
{
|
|
Contents = LoadClientContents(CacheFile);
|
|
}
|
|
else
|
|
{
|
|
Contents = FindAndSaveClientContents(Client, ClientName, ChangeNumber, bFakeSync, CacheFile);
|
|
}
|
|
}
|
|
|
|
// Sync all the appropriate files
|
|
RemoveFilesFromWorkspace(Contents);
|
|
AddFilesToWorkspace(Client, Contents, bFakeSync);
|
|
}
|
|
|
|
Log.TraceInformation("Completed in {0:0.0}s", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the cache with the head revision of the given streams.
|
|
/// </summary>
|
|
public void Populate(List<KeyValuePair<string, string>> ClientAndStreamNames, List<string> Filters, bool bFakeSync)
|
|
{
|
|
Log.TraceInformation("Populating with {0} streams", ClientAndStreamNames.Count);
|
|
using(new LogIndentScope(" "))
|
|
{
|
|
// Clean the current workspace
|
|
Clean();
|
|
|
|
// Update the list of files in each stream
|
|
Tuple<int, StreamDirectoryInfo>[] StreamState = new Tuple<int, StreamDirectoryInfo>[ClientAndStreamNames.Count];
|
|
for(int Idx = 0; Idx < ClientAndStreamNames.Count; Idx++)
|
|
{
|
|
string ClientName = ClientAndStreamNames[Idx].Key;
|
|
string StreamName = ClientAndStreamNames[Idx].Value;
|
|
Log.TraceInformation("Finding contents of {0}:", StreamName);
|
|
|
|
using(new LogIndentScope(" "))
|
|
{
|
|
PerforceConnection Client = UpdateClient(ClientName, StreamName);
|
|
|
|
int ChangeNumber = GetLatestClientChange(Client, ClientName);
|
|
Log.TraceInformation("Latest change is CL {0}", ChangeNumber);
|
|
|
|
RevertInternal(Client, ClientName);
|
|
ClearClientHaveTable(Client, ClientName);
|
|
UpdateClientHaveTable(Client, ClientName, ChangeNumber, Filters);
|
|
|
|
StreamDirectoryInfo Contents = FindClientContents(Client, ClientName, ChangeNumber, bFakeSync);
|
|
StreamState[Idx] = Tuple.Create(ChangeNumber, Contents);
|
|
|
|
GC.Collect();
|
|
}
|
|
}
|
|
|
|
// Remove any files from the workspace not referenced by the first stream. This ensures we can purge things from the cache that we no longer need.
|
|
if(ClientAndStreamNames.Count > 0)
|
|
{
|
|
RemoveFilesFromWorkspace(StreamState[0].Item2);
|
|
}
|
|
|
|
// Shrink the contents of the cache
|
|
using(LogStatusScope Status = new LogStatusScope("Updating cache..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
HashSet<FileContentId> CommonContentIds = new HashSet<FileContentId>();
|
|
Dictionary<FileContentId, long> ContentIdToLength = new Dictionary<FileContentId, long>();
|
|
for(int Idx = 0; Idx < ClientAndStreamNames.Count; Idx++)
|
|
{
|
|
List<StreamFileInfo> Files = StreamState[Idx].Item2.GetFiles();
|
|
foreach(StreamFileInfo File in Files)
|
|
{
|
|
ContentIdToLength[File.ContentId] = File.Length;
|
|
}
|
|
|
|
if(Idx == 0)
|
|
{
|
|
CommonContentIds.UnionWith(Files.Select(x => x.ContentId));
|
|
}
|
|
else
|
|
{
|
|
CommonContentIds.IntersectWith(Files.Select(x => x.ContentId));
|
|
}
|
|
}
|
|
|
|
List<TrackedFileInfo> TrackedFiles = ContentIdToTrackedFile.Values.ToList();
|
|
foreach(TrackedFileInfo TrackedFile in TrackedFiles)
|
|
{
|
|
if(!ContentIdToLength.ContainsKey(TrackedFile.ContentId))
|
|
{
|
|
RemoveTrackedFile(TrackedFile);
|
|
}
|
|
}
|
|
|
|
GC.Collect();
|
|
|
|
double TotalSize = ContentIdToLength.Sum(x => x.Value) / (1024.0 * 1024.0);
|
|
Status.SetProgress("{0:n1}mb total, {1:n1}mb differences ({2:0.0}s)", TotalSize, TotalSize - CommonContentIds.Sum(x => ContentIdToLength[x]) / (1024.0 * 1024.0), Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
// Sync all the new files
|
|
for(int Idx = 0; Idx < ClientAndStreamNames.Count; Idx++)
|
|
{
|
|
string ClientName = ClientAndStreamNames[Idx].Key;
|
|
string StreamName = ClientAndStreamNames[Idx].Value;
|
|
Log.TraceInformation("Syncing files for {0}:", StreamName);
|
|
|
|
using(new LogIndentScope(" "))
|
|
{
|
|
PerforceConnection Client = UpdateClient(ClientName, StreamName);
|
|
|
|
int ChangeNumber = StreamState[Idx].Item1;
|
|
UpdateClientHaveTable(Client, ClientName, ChangeNumber, Filters);
|
|
|
|
StreamDirectoryInfo Contents = StreamState[Idx].Item2;
|
|
RemoveFilesFromWorkspace(Contents);
|
|
AddFilesToWorkspace(Client, Contents, bFakeSync);
|
|
}
|
|
}
|
|
|
|
// Save the new repo state
|
|
Save(RepositoryState.Clean);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Core operations
|
|
|
|
/// <summary>
|
|
/// Sets the stream for the current client
|
|
/// </summary>
|
|
/// <param name="ClientName">Name of the client</param>
|
|
/// <param name="StreamName">New stream for the client</param>
|
|
private PerforceConnection UpdateClient(string ClientName, string StreamName)
|
|
{
|
|
// Create or update the client if it doesn't exist already
|
|
if(ClientNames.Add(ClientName))
|
|
{
|
|
using(LogStatusScope Status = new LogStatusScope("Updating client..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
ClientRecord Client = new ClientRecord();
|
|
Client.Name = ClientName;
|
|
Client.Owner = UserName;
|
|
Client.Host = HostName;
|
|
Client.Stream = StreamName;
|
|
Client.Root = WorkspaceDir.FullName;
|
|
Client.Type = "partitioned";
|
|
|
|
PerforceResponse Response = Perforce.CreateClient(Client);
|
|
if(!Response.Succeeded)
|
|
{
|
|
Perforce.DeleteClient(DeleteClientOptions.None, ClientName);
|
|
Perforce.CreateClient(Client).RequireSuccess();
|
|
}
|
|
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Update the config file with the name of the client
|
|
FileReference ConfigFile = FileReference.Combine(BaseDir, "p4.ini");
|
|
using(StreamWriter Writer = new StreamWriter(ConfigFile.FullName))
|
|
{
|
|
Writer.WriteLine("P4CLIENT={0}", ClientName);
|
|
}
|
|
|
|
// Return a Perforce connection for this client
|
|
return CreatePerforceConnection(ServerAndPort, UserName, ClientName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the latest change number in the current client
|
|
/// </summary>
|
|
/// <param name="Client">The perforce client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
/// <returns>The latest submitted change number</returns>
|
|
private int GetLatestClientChange(PerforceConnection Client, string ClientName)
|
|
{
|
|
int ChangeNumber;
|
|
using(LogStatusScope Status = new LogStatusScope("Finding latest change..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
PerforceResponseList<ChangesRecord> Changes = Client.Changes(ChangesOptions.None, 1, ChangeStatus.Submitted, String.Format("//{0}/...", ClientName));
|
|
Changes.RequireSuccess();
|
|
ChangeNumber = Changes[0].Data.Number;
|
|
|
|
Status.SetProgress("CL {0} ({1:0.0}s)", ChangeNumber, Timer.Elapsed.TotalSeconds);
|
|
}
|
|
return ChangeNumber;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Revert all files that are open in the current workspace. Does not replace them with valid revisions.
|
|
/// </summary>
|
|
/// <param name="Client">The current client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
private void RevertInternal(PerforceConnection Client, string ClientName)
|
|
{
|
|
using(LogStatusScope Status = new LogStatusScope("Reverting changes..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Get a list of open files
|
|
PerforceResponseList<FStatRecord> OpenedFilesResponse = Client.Opened(OpenedOptions.ShortOutput, -1, ClientName, null, 1);
|
|
OpenedFilesResponse.RequireSuccess();
|
|
|
|
// If there are any files, revert them
|
|
if(OpenedFilesResponse.Any())
|
|
{
|
|
Client.Revert(-1, null, RevertOptions.KeepWorkspaceFiles, "//...").RequireSuccess();
|
|
}
|
|
|
|
// Find all the open changes
|
|
PerforceResponseList<ChangesRecord> Changes = Client.Changes(ChangesOptions.None, ClientName, -1, ChangeStatus.Pending, null);
|
|
Changes.RequireSuccess();
|
|
|
|
// Delete the changelist
|
|
foreach(ChangesRecord Change in Changes.Data)
|
|
{
|
|
// Find a list of shelved changes
|
|
PerforceResponseList<DescribeRecord> DescribeResponse = Client.Describe(DescribeOptions.Shelved, -1, Change.Number);
|
|
DescribeResponse.RequireSuccess();
|
|
|
|
// Delete the shelved files
|
|
foreach(DescribeRecord Record in DescribeResponse.Data)
|
|
{
|
|
if(Record.Files.Count > 0)
|
|
{
|
|
Client.DeleteShelvedFiles(Record.Number).RequireSuccess();
|
|
}
|
|
}
|
|
|
|
// Delete the changelist
|
|
Client.DeleteChange(DeleteChangeOptions.None, Change.Number).RequireSuccess();
|
|
}
|
|
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the have table. This ensures that we'll always fetch the names of files at head revision, which aren't updated otherwise.
|
|
/// </summary>
|
|
/// <param name="Client">The client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
private void ClearClientHaveTable(PerforceConnection Client, string ClientName)
|
|
{
|
|
using(LogStatusScope Scope = new LogStatusScope("Clearing have table..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
Client.SyncQuiet(SyncOptions.KeepWorkspaceFiles, -1, String.Format("//{0}/...#0", ClientName)).RequireSuccess();
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the have table to reflect the given stream
|
|
/// </summary>
|
|
/// <param name="Client">The client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
/// <param name="ChangeNumber">The change number to sync. May be -1, for latest.</param>
|
|
/// <param name="Filters">List of filters to apply to the workspace. Each entry should be a path relative to the stream root, with an optional '-'prefix.</param>
|
|
private void UpdateClientHaveTable(PerforceConnection Client, string ClientName, int ChangeNumber, List<string> Filters)
|
|
{
|
|
using(LogStatusScope Scope = new LogStatusScope("Updating have table..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Sync an initial set of files. Either start with a full workspace and remove files, or start with nothing and add files.
|
|
if(Filters.Count == 0 || Filters[0].StartsWith("-"))
|
|
{
|
|
Client.SyncQuiet(SyncOptions.KeepWorkspaceFiles, -1, String.Format("//{0}/...@{1}", ClientName, ChangeNumber)).RequireSuccess();
|
|
}
|
|
else
|
|
{
|
|
Client.SyncQuiet(SyncOptions.KeepWorkspaceFiles, -1, String.Format("//{0}/...#0", ClientName)).RequireSuccess();
|
|
}
|
|
|
|
// Update with the contents of each filter
|
|
foreach(string Filter in Filters)
|
|
{
|
|
string SyncPath;
|
|
if(Filter.StartsWith("-"))
|
|
{
|
|
SyncPath = String.Format("//{0}/{1}#0", ClientName, RemoveLeadingSlash(Filter.Substring(1)));
|
|
}
|
|
else
|
|
{
|
|
SyncPath = String.Format("//{0}/{1}@{2}", ClientName, RemoveLeadingSlash(Filter), ChangeNumber);
|
|
}
|
|
Client.SyncQuiet(SyncOptions.KeepWorkspaceFiles, -1, SyncPath);
|
|
}
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the contents of the client, as synced.
|
|
/// </summary>
|
|
/// <param name="Client">The client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
/// <param name="ChangeNumber">The change number being synced. This must be specified in order to get the digest at the correct revision.</param>
|
|
/// <param name="bFakeSync">Whether this is for a fake sync. Poisons the file type to ensure that the cache is not corrupted.</param>
|
|
private StreamDirectoryInfo FindClientContents(PerforceConnection Client, string ClientName, int ChangeNumber, bool bFakeSync)
|
|
{
|
|
StreamDirectoryInfo Contents = new StreamDirectoryInfo();
|
|
using(LogStatusScope Scope = new LogStatusScope("Fetching metadata..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Find all the files in the branch
|
|
PerforceResponseList<FStatRecord> Response = Client.FStat(FStatOptions.OnlyHave | FStatOptions.IncludeFileSizes | FStatOptions.ClientFileInPerforceSyntax | FStatOptions.ShortenOutput, string.Format("//{0}/...@{1}", ClientName, ChangeNumber));
|
|
Response.RequireSuccess();
|
|
|
|
// Get the expected prefix for all files in client syntax
|
|
string ClientPrefix = string.Format("//{0}/", ClientName);
|
|
|
|
// Create the workspace, and add records for all the files. Exclude deleted files with digest = null.
|
|
foreach(FStatRecord Record in Response.Data)
|
|
{
|
|
if(!Record.ClientFile.StartsWith(ClientPrefix))
|
|
{
|
|
throw new InvalidDataException(String.Format("Client path returned by Perforce ('{0}') does not begin with client name ('{1}')", Record.ClientFile, ClientPrefix));
|
|
}
|
|
if(Record.Digest != null)
|
|
{
|
|
StreamDirectoryInfo LastStreamDirectory = Contents;
|
|
|
|
string[] Fragments = PerforceUtils.UnescapePath(Record.ClientFile.Substring(ClientPrefix.Length)).Split('/');
|
|
for(int Idx = 0; Idx < Fragments.Length - 1; Idx++)
|
|
{
|
|
StreamDirectoryInfo NextStreamDirectory;
|
|
if(!LastStreamDirectory.NameToSubDirectory.TryGetValue(Fragments[Idx], out NextStreamDirectory))
|
|
{
|
|
NextStreamDirectory = new StreamDirectoryInfo(Fragments[Idx], LastStreamDirectory);
|
|
LastStreamDirectory.NameToSubDirectory.Add(Fragments[Idx], NextStreamDirectory);
|
|
}
|
|
LastStreamDirectory = NextStreamDirectory;
|
|
}
|
|
|
|
byte[] Digest = StringUtils.ParseHexString(Record.Digest);
|
|
FileContentId ContentId = new FileContentId(Digest, Record.HeadType + (bFakeSync? "+fake" : ""));
|
|
|
|
string DepotFileAndRevision = String.Format("{0}#{1}", Record.DepotFile, Record.HaveRevision);
|
|
LastStreamDirectory.NameToFile.Add(Fragments[Fragments.Length - 1], new StreamFileInfo(Fragments[Fragments.Length - 1], Record.FileSize, ContentId, LastStreamDirectory, DepotFileAndRevision));
|
|
}
|
|
}
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
return Contents;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the contents of a client from disk
|
|
/// </summary>
|
|
/// <param name="CacheFile">The cache file to read from</param>
|
|
/// <returns>Contents of the workspace</returns>
|
|
StreamDirectoryInfo LoadClientContents(FileReference CacheFile)
|
|
{
|
|
StreamDirectoryInfo Contents;
|
|
using(LogStatusScope Scope = new LogStatusScope("Reading cached metadata from {0}...", CacheFile))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
Contents = StreamDirectoryInfo.Load(CacheFile);
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
return Contents;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the contents of a workspace, and saves it to disk
|
|
/// </summary>
|
|
/// <param name="Client">The client connection</param>
|
|
/// <param name="ClientName">Name of the current client</param>
|
|
/// <param name="ChangeNumber">The change number being synced. This must be specified in order to get the digest at the correct revision.</param>
|
|
/// <param name="bFakeSync">Whether this is for a fake sync. Poisons the file type to ensure that the cache is not corrupted.</param>
|
|
/// <param name="CacheFile">Location of the file to save the cached contents</param>
|
|
/// <returns>Contents of the workspace</returns>
|
|
private StreamDirectoryInfo FindAndSaveClientContents(PerforceConnection Client, string ClientName, int ChangeNumber, bool bFakeSync, FileReference CacheFile)
|
|
{
|
|
StreamDirectoryInfo Contents = FindClientContents(Client, ClientName, ChangeNumber, bFakeSync);
|
|
using(LogStatusScope Scope = new LogStatusScope("Saving metadata to {0}...", CacheFile))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Handle the case where two machines may try to write to the cache file at once by writing to a temporary file
|
|
FileReference TempCacheFile = new FileReference(String.Format("{0}.{1}", CacheFile, Guid.NewGuid()));
|
|
Contents.Save(TempCacheFile);
|
|
|
|
// Try to move it into place
|
|
try
|
|
{
|
|
FileReference.Move(TempCacheFile, CacheFile);
|
|
}
|
|
catch(IOException)
|
|
{
|
|
if(!FileReference.Exists(CacheFile))
|
|
{
|
|
throw;
|
|
}
|
|
FileReference.Delete(TempCacheFile);
|
|
}
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
return Contents;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove files from the workspace
|
|
/// </summary>
|
|
/// <param name="Contents">Contents of the target stream</param>
|
|
private void RemoveFilesFromWorkspace(StreamDirectoryInfo Contents)
|
|
{
|
|
// Make sure the repair flag is clear before we start
|
|
RunOptionalRepair();
|
|
|
|
// Figure out what to remove
|
|
WorkspaceTransactionRemove Transaction;
|
|
using(LogStatusScope Scope = new LogStatusScope("Gathering files to remove..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Transaction = new WorkspaceTransactionRemove(Workspace, Contents, ContentIdToTrackedFile);
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
// Move files into the cache
|
|
KeyValuePair<FileContentId, WorkspaceFileInfo>[] FilesToMove = Transaction.FilesToMove.ToArray();
|
|
if(FilesToMove.Length > 0)
|
|
{
|
|
using(LogStatusScope Scope = new LogStatusScope("Moving {0} {1} to cache...", FilesToMove.Length, (FilesToMove.Length == 1)? "file" : "files"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
// Add any new files to the cache
|
|
List<KeyValuePair<FileReference, FileReference>> SourceAndTargetFiles = new List<KeyValuePair<FileReference, FileReference>>();
|
|
foreach(KeyValuePair<FileContentId, WorkspaceFileInfo> FileToMove in FilesToMove)
|
|
{
|
|
ulong CacheId = GetUniqueCacheId(FileToMove.Key);
|
|
TrackedFileInfo NewTrackingInfo = new TrackedFileInfo(CacheDir, FileToMove.Key, CacheId, FileToMove.Value.Length, FileToMove.Value.LastModifiedTicks, FileToMove.Value.bReadOnly, NextSequenceNumber);
|
|
ContentIdToTrackedFile.Add(FileToMove.Key, NewTrackingInfo);
|
|
SourceAndTargetFiles.Add(new KeyValuePair<FileReference, FileReference>(FileToMove.Value.GetLocation(), NewTrackingInfo.GetLocation()));
|
|
}
|
|
NextSequenceNumber++;
|
|
|
|
// Save the current state of the repository as dirty. If we're interrupted, we will have two places to check for each file (the cache and workspace).
|
|
Save(RepositoryState.Dirty);
|
|
|
|
// Execute all the moves and deletes
|
|
Parallel.ForEach(SourceAndTargetFiles, SourceAndTargetFile => FileUtils.ForceMoveFile(SourceAndTargetFile.Key, SourceAndTargetFile.Value));
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Remove files which are no longer needed
|
|
WorkspaceFileInfo[] FilesToDelete = Transaction.FilesToDelete.ToArray();
|
|
if(FilesToDelete.Length > 0)
|
|
{
|
|
using(LogStatusScope Scope = new LogStatusScope("Deleting {0} {1}...", FilesToDelete.Length, (FilesToDelete.Length == 1)? "file" : "files"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Parallel.ForEach(FilesToDelete, FileToDelete => RemoveFile(FileToDelete));
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Remove directories which are no longer needed
|
|
WorkspaceDirectoryInfo[] DirectoriesToDelete = Transaction.DirectoriesToDelete.ToArray();
|
|
if(DirectoriesToDelete.Length > 0)
|
|
{
|
|
using(LogStatusScope Scope = new LogStatusScope("Deleting {0} {1}...", DirectoriesToDelete.Length, (DirectoriesToDelete.Length == 1)? "directory" : "directories"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
foreach(string DirectoryToDelete in DirectoriesToDelete.Select(x => x.GetFullName()).OrderByDescending(x => x.Length))
|
|
{
|
|
RemoveDirectory(DirectoryToDelete);
|
|
}
|
|
|
|
Scope.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Update the workspace and save the new state
|
|
Workspace = Transaction.NewWorkspaceRootDir;
|
|
Save(RepositoryState.Clean);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function to delete a file from the workspace, and output any failure as a warning.
|
|
/// </summary>
|
|
/// <param name="FileToDelete">The file to be deleted</param>
|
|
void RemoveFile(WorkspaceFileInfo FileToDelete)
|
|
{
|
|
try
|
|
{
|
|
FileUtils.ForceDeleteFile(FileToDelete.GetLocation());
|
|
}
|
|
catch(Exception Ex)
|
|
{
|
|
Log.TraceWarning("warning: Unable to delete file {0}.", FileToDelete.GetFullName());
|
|
Log.TraceVerbose(ExceptionUtils.FormatExceptionDetails(Ex));
|
|
bRequiresRepair = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function to delete a directory from the workspace, and output any failure as a warning.
|
|
/// </summary>
|
|
/// <param name="DirectoryToDelete">The directory to be deleted</param>
|
|
void RemoveDirectory(string DirectoryToDelete)
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(DirectoryToDelete, false);
|
|
}
|
|
catch(Exception Ex)
|
|
{
|
|
Log.TraceWarning("warning: Unable to delete directory {0}", DirectoryToDelete);
|
|
Log.TraceVerbose(ExceptionUtils.FormatExceptionDetails(Ex));
|
|
bRequiresRepair = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the workspace to match the given stream, syncing files and moving to/from the cache as necessary.
|
|
/// </summary>
|
|
/// <param name="Client">The client connection</param>
|
|
/// <param name="Stream">Contents of the stream</param>
|
|
/// <param name="bFakeSync">Whether to simulate the sync operation, rather than actually syncing files</param>
|
|
private void AddFilesToWorkspace(PerforceConnection Client, StreamDirectoryInfo Stream, bool bFakeSync)
|
|
{
|
|
// Make sure the repair flag is reset
|
|
RunOptionalRepair();
|
|
|
|
// Figure out what we need to do
|
|
WorkspaceTransactionAdd Transaction;
|
|
using(LogStatusScope Status = new LogStatusScope("Gathering files to add..."))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
Transaction = new WorkspaceTransactionAdd(Workspace, Stream, ContentIdToTrackedFile);
|
|
Workspace = Transaction.NewWorkspaceRootDir;
|
|
Save(RepositoryState.Dirty);
|
|
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
// Swap files in and out of the cache
|
|
WorkspaceFileToMove[] FilesToMove = Transaction.FilesToMove.Values.ToArray();
|
|
if(FilesToMove.Length > 0)
|
|
{
|
|
using(LogStatusScope Status = new LogStatusScope("Moving {0} {1} from cache...", FilesToMove.Length, (FilesToMove.Length == 1)? "file" : "files"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
Parallel.ForEach(FilesToMove, FileToMove => MoveFileFromCache(FileToMove, Transaction.FilesToSync));
|
|
ContentIdToTrackedFile = ContentIdToTrackedFile.Where(x => !Transaction.FilesToMove.ContainsKey(x.Value)).ToDictionary(x => x.Key, x => x.Value);
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Swap files in and out of the cache
|
|
WorkspaceFileToCopy[] FilesToCopy = Transaction.FilesToCopy.ToArray();
|
|
if(FilesToCopy.Length > 0)
|
|
{
|
|
using(LogStatusScope Status = new LogStatusScope("Copying {0} {1} within workspace...", FilesToCopy.Length, (FilesToCopy.Length == 1)? "file" : "files"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
Parallel.ForEach(FilesToCopy, FileToCopy => CopyFileWithinWorkspace(FileToCopy, Transaction.FilesToSync));
|
|
Status.SetProgress("({0:0.0}s)", Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Find all the files we want to sync
|
|
WorkspaceFileToSync[] FilesToSync = Transaction.FilesToSync.ToArray();
|
|
if(FilesToSync.Length > 0)
|
|
{
|
|
// Sync all the files
|
|
long TotalSize = FilesToSync.Sum(x => x.StreamFile.Length);
|
|
using(LogStatusScope Status = new LogStatusScope("Syncing {0} {1}...", FilesToSync.Length, (FilesToSync.Length == 1)? "file" : "files"))
|
|
{
|
|
Stopwatch Timer = Stopwatch.StartNew();
|
|
|
|
long SyncedSize = 0;
|
|
for(int EndIdx = 0; EndIdx < FilesToSync.Length; )
|
|
{
|
|
int BeginIdx = EndIdx;
|
|
|
|
// Update the progress
|
|
Status.SetProgress("{0:n1}% ({1:n1}mb/{2:n1}mb)", SyncedSize * 100.0 / TotalSize, SyncedSize / (1024.0 * 1024.0), TotalSize / (1024.0 * 1024.0));
|
|
|
|
// Figure out the next batch of files to sync
|
|
long BatchSize = 0;
|
|
for(; EndIdx < FilesToSync.Length && BatchSize < 256 * 1024 * 1024; EndIdx++)
|
|
{
|
|
BatchSize += FilesToSync[EndIdx].StreamFile.Length;
|
|
}
|
|
|
|
// Print the list of files we're syncing to the log
|
|
Log.TraceLog("Batch {0}-{1}, {2:n1}mb:", BeginIdx, EndIdx, BatchSize / (1024.0 * 1024.0));
|
|
for(int Idx = BeginIdx; Idx < EndIdx; Idx++)
|
|
{
|
|
Log.TraceLog(" {0,-128} [{1,-48}]", FilesToSync[Idx].StreamFile.DepotFileAndRevision, FilesToSync[Idx].StreamFile.ContentId);
|
|
}
|
|
|
|
// Sync the files
|
|
if(bFakeSync)
|
|
{
|
|
for(int Idx = BeginIdx; Idx < EndIdx; Idx++)
|
|
{
|
|
FileReference LocalFile = FilesToSync[Idx].WorkspaceFile.GetLocation();
|
|
DirectoryReference.CreateDirectory(LocalFile.Directory);
|
|
FileReference.WriteAllBytes(LocalFile, new byte[0]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FileReference SyncFileName = FileReference.Combine(BaseDir, "SyncList.txt");
|
|
|
|
using(StreamWriter Writer = new StreamWriter(SyncFileName.FullName))
|
|
{
|
|
for(int Idx = BeginIdx; Idx < EndIdx; Idx++)
|
|
{
|
|
Writer.WriteLine(FilesToSync[Idx].StreamFile.DepotFileAndRevision);
|
|
}
|
|
}
|
|
|
|
PerforceConnection TempPerforce = new PerforceConnection(String.Format("{0} -x\"{1}\"", Client.GlobalOptions, SyncFileName));
|
|
TempPerforce.Sync(SyncOptions.Force | SyncOptions.FullDepotSyntax, -1).RequireSuccess();
|
|
}
|
|
|
|
// Update metadata for the current state
|
|
Parallel.For(BeginIdx, EndIdx, Idx => FilesToSync[Idx].WorkspaceFile.UpdateMetadata());
|
|
|
|
// Save the current state
|
|
Save(RepositoryState.Dirty);
|
|
|
|
// Update the status
|
|
SyncedSize += BatchSize;
|
|
}
|
|
|
|
Status.SetProgress("100% ({0:n1}mb) ({1:0.0}s)", TotalSize / (1024 * 1024.0), Timer.Elapsed.TotalSeconds);
|
|
}
|
|
}
|
|
|
|
// Save the clean state
|
|
Workspace = Transaction.NewWorkspaceRootDir;
|
|
Save(RepositoryState.Clean);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function to move a file from the cache into the workspace. If it fails, adds the file to a list to be synced.
|
|
/// </summary>
|
|
/// <param name="FileToMove">Information about the file to move</param>
|
|
/// <param name="FilesToSync">List of files to be synced. If the move fails, the file will be added to this list of files to sync.</param>
|
|
void MoveFileFromCache(WorkspaceFileToMove FileToMove, ConcurrentBag<WorkspaceFileToSync> FilesToSync)
|
|
{
|
|
try
|
|
{
|
|
FileReference.Move(FileToMove.TrackedFile.GetLocation(), FileToMove.WorkspaceFile.GetLocation());
|
|
}
|
|
catch(Exception Ex)
|
|
{
|
|
Log.TraceWarning("warning: Unable to move {0} from cache to {1}. Syncing instead.", FileToMove.TrackedFile.GetLocation(), FileToMove.WorkspaceFile.GetLocation());
|
|
Log.TraceVerbose(ExceptionUtils.FormatExceptionDetails(Ex));
|
|
FilesToSync.Add(new WorkspaceFileToSync(FileToMove.StreamFile, FileToMove.WorkspaceFile));
|
|
bRequiresRepair = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function to copy a file within the workspace. If it fails, adds the file to a list to be synced.
|
|
/// </summary>
|
|
/// <param name="FileToCopy">Information about the file to move</param>
|
|
/// <param name="FilesToSync">List of files to be synced. If the move fails, the file will be added to this list of files to sync.</param>
|
|
void CopyFileWithinWorkspace(WorkspaceFileToCopy FileToCopy, ConcurrentBag<WorkspaceFileToSync> FilesToSync)
|
|
{
|
|
try
|
|
{
|
|
FileReference.Copy(FileToCopy.SourceWorkspaceFile.GetLocation(), FileToCopy.TargetWorkspaceFile.GetLocation());
|
|
FileToCopy.TargetWorkspaceFile.UpdateMetadata();
|
|
}
|
|
catch(Exception Ex)
|
|
{
|
|
Log.TraceWarning("warning: Unable to copy {0} to {1}. Syncing instead.", FileToCopy.SourceWorkspaceFile.GetLocation(), FileToCopy.TargetWorkspaceFile.GetLocation());
|
|
Log.TraceVerbose(ExceptionUtils.FormatExceptionDetails(Ex));
|
|
FilesToSync.Add(new WorkspaceFileToSync(FileToCopy.StreamFile, FileToCopy.TargetWorkspaceFile));
|
|
bRequiresRepair = true;
|
|
}
|
|
}
|
|
|
|
void RemoveTrackedFile(TrackedFileInfo TrackedFile)
|
|
{
|
|
ContentIdToTrackedFile.Remove(TrackedFile.ContentId);
|
|
CacheEntries.Remove(TrackedFile.CacheId);
|
|
FileUtils.ForceDeleteFile(TrackedFile.GetLocation());
|
|
}
|
|
|
|
void CreateCacheHierarchy()
|
|
{
|
|
for(int IdxA = 0; IdxA < 16; IdxA++)
|
|
{
|
|
DirectoryReference DirA = DirectoryReference.Combine(CacheDir, String.Format("{0:X}", IdxA));
|
|
DirectoryReference.CreateDirectory(DirA);
|
|
for(int IdxB = 0; IdxB < 16; IdxB++)
|
|
{
|
|
DirectoryReference DirB = DirectoryReference.Combine(DirA, String.Format("{0:X}", IdxB));
|
|
DirectoryReference.CreateDirectory(DirB);
|
|
for(int IdxC = 0; IdxC < 16; IdxC++)
|
|
{
|
|
DirectoryReference DirC = DirectoryReference.Combine(DirB, String.Format("{0:X}", IdxC));
|
|
DirectoryReference.CreateDirectory(DirC);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines a unique cache id for a file content id
|
|
/// </summary>
|
|
/// <param name="ContentId">File content id to get a unique id for</param>
|
|
/// <returns>The unique cache id</returns>
|
|
ulong GetUniqueCacheId(FileContentId ContentId)
|
|
{
|
|
// Initialize the cache id to the top 16 bytes of the digest, then increment it until we find a unique id
|
|
ulong CacheId = 0;
|
|
for(int Idx = 0; Idx < 8; Idx++)
|
|
{
|
|
CacheId = (CacheId << 8) | ContentId.Digest[Idx];
|
|
}
|
|
while(!CacheEntries.Add(CacheId))
|
|
{
|
|
CacheId++;
|
|
}
|
|
return CacheId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sanitizes a string for use in a Perforce client name
|
|
/// </summary>
|
|
/// <param name="Text">Text to sanitize</param>
|
|
/// <returns>Sanitized text</returns>
|
|
static string Sanitize(string Text)
|
|
{
|
|
StringBuilder Result = new StringBuilder();
|
|
for(int Idx = 0; Idx < Text.Length; Idx++)
|
|
{
|
|
if((Text[Idx] >= '0' && Text[Idx] <= '9') || (Text[Idx] >= 'a' && Text[Idx] <= 'z') || (Text[Idx] >= 'A' && Text[Idx] <= 'Z') || Text[Idx] == '-' || Text[Idx] == '_' || Text[Idx] == '.')
|
|
{
|
|
Result.Append(Text[Idx]);
|
|
}
|
|
}
|
|
return Result.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the leading slash from a path
|
|
/// </summary>
|
|
/// <param name="Path">The path to remove a slash from</param>
|
|
/// <returns>The path without a leading slash</returns>
|
|
static string RemoveLeadingSlash(string Path)
|
|
{
|
|
if(Path.Length > 0 && Path[0] == '/')
|
|
{
|
|
return Path.Substring(1);
|
|
}
|
|
else
|
|
{
|
|
return Path;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the path to a backup file used while a new file is being written out
|
|
/// </summary>
|
|
/// <param name="TargetFile">The file being written to</param>
|
|
/// <returns>The path to a backup file</returns>
|
|
private static FileReference GetBackupFile(FileReference TargetFile)
|
|
{
|
|
return new FileReference(TargetFile.FullName + ".transaction");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins a write transaction on the given file. Assumes only one process will be reading/writing at a time, but the operation can be interrupted.
|
|
/// </summary>
|
|
/// <param name="TargetFile">The file being written to</param>
|
|
public static void BeginTransaction(FileReference TargetFile)
|
|
{
|
|
FileReference TransactionFile = GetBackupFile(TargetFile);
|
|
if (FileReference.Exists(TargetFile))
|
|
{
|
|
FileUtils.ForceMoveFile(TargetFile, TransactionFile);
|
|
}
|
|
else if (FileReference.Exists(TransactionFile))
|
|
{
|
|
FileUtils.ForceDeleteFile(TransactionFile);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mark a transaction on the given file as complete, and removes the backup file.
|
|
/// </summary>
|
|
/// <param name="TargetFile">The file being written to</param>
|
|
public static void CompleteTransaction(FileReference TargetFile)
|
|
{
|
|
FileReference TransactionFile = GetBackupFile(TargetFile);
|
|
FileUtils.ForceDeleteFile(TransactionFile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores the backup for a target file, if it exists. This allows recovery from an incomplete transaction.
|
|
/// </summary>
|
|
/// <param name="TargetFile">The file being written to</param>
|
|
public static void RestoreBackup(FileReference TargetFile)
|
|
{
|
|
FileReference TransactionFile = GetBackupFile(TargetFile);
|
|
if (FileReference.Exists(TransactionFile))
|
|
{
|
|
FileUtils.ForceMoveFile(TransactionFile, TargetFile);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|