Files
UnrealEngineUWP/Engine/Source/Programs/Shared/EpicGames.Perforce.Managed/ManagedWorkspace.cs
Ben Marsh f1e04dc894 Horde: Fix incorrectly named enum members, causing syncing to return invalid records.
#preflight none

[CL 19528834 by Ben Marsh in ue5-main branch]
2022-03-28 12:18:07 -04:00

2025 lines
75 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.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
namespace EpicGames.Perforce.Managed
{
/// <summary>
/// Exception thrown when there is not enough free space on the drive
/// </summary>
public class InsufficientSpaceException : Exception
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="message">Error message</param>
public InsufficientSpaceException(string message)
: base(message)
{
}
}
/// <summary>
/// Information about a populate request
/// </summary>
public class PopulateRequest
{
/// <summary>
/// The Perforce connection
/// </summary>
public IPerforceConnection _perforceClient;
/// <summary>
/// Stream to sync it to
/// </summary>
public string _streamName;
/// <summary>
/// View for this client
/// </summary>
public IReadOnlyList<string> _view;
/// <summary>
/// Constructor
/// </summary>
/// <param name="perforceClient">The perforce connection</param>
/// <param name="streamName">Stream to be synced</param>
/// <param name="view">List of filters for the stream</param>
public PopulateRequest(IPerforceConnection perforceClient, string streamName, IReadOnlyList<string> view)
{
_perforceClient = perforceClient;
_streamName = streamName;
_view = view;
}
}
/// <summary>
/// Version number for managed workspace cache files
/// </summary>
enum ManagedWorkspaceVersion
{
/// <summary>
/// Initial version number
/// </summary>
Initial = 2,
/// <summary>
/// Including stream directory digests in workspace directories
/// </summary>
AddDigest = 3,
/// <summary>
/// Changing hash algorithm from SHA1 to IoHash
/// </summary>
AddDigestIoHash = 4,
}
/// <summary>
/// Represents a repository of streams and cached data
/// </summary>
public class ManagedWorkspace
{
/// <summary>
/// The current transaction state. Used to determine whether a repository needs to be cleaned on startup.
/// </summary>
enum TransactionState
{
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) | 2;
/// <summary>
/// The current revision number for cache archives.
/// </summary>
static int CurrentVersion { get; } = Enum.GetValues(typeof(ManagedWorkspaceVersion)).Cast<int>().Max();
/// <summary>
/// Maximum number of threads to sync in parallel
/// </summary>
const int NumParallelSyncThreads = 4;
/// <summary>
/// Minimum amount of space that must be on a drive after a branch is synced
/// </summary>
const long MinScratchSpace = 50L * 1024 * 1024 * 1024;
/// <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 host
/// </summary>
readonly 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>
/// The log output device
/// </summary>
readonly ILogger _logger;
/// <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>
readonly Dictionary<string, ClientRecord> _createdClients = new Dictionary<string, ClientRecord>();
/// <summary>
/// Set of unique cache entries. We use this to ensure new names in the cache are unique.
/// </summary>
readonly 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, CachedFileInfo> _contentIdToTrackedFile = new Dictionary<FileContentId, CachedFileInfo>();
/// <summary>
/// Constructor
/// </summary>
/// <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>
/// <param name="logger">The log output device</param>
private ManagedWorkspace(string hostName, uint nextSequenceNumber, DirectoryReference baseDir, ILogger logger)
{
// Save the Perforce settings
_hostName = hostName;
_nextSequenceNumber = nextSequenceNumber;
_logger = logger;
// Get all the directories
_baseDir = baseDir;
DirectoryReference.CreateDirectory(baseDir);
_cacheDir = DirectoryReference.Combine(baseDir, "Cache");
DirectoryReference.CreateDirectory(_cacheDir);
_workspaceDir = DirectoryReference.Combine(baseDir, "Sync");
DirectoryReference.CreateDirectory(_workspaceDir);
// Create the workspace
_workspace = new WorkspaceDirectoryInfo(_workspaceDir);
}
/// <summary>
/// Loads a repository from the given directory, or create it if it doesn't exist
/// </summary>
/// <param name="hostName">Name of the current machine. Will be automatically detected from the host settings if not present.</param>
/// <param name="baseDir">The base directory for the repository</param>
/// <param name="bOverwrite">Whether to allow overwriting a repository that's not up to date</param>
/// <param name="logger">The logging interface</param>
/// <param name="cancellationToken">Cancellation token for this operation</param>
/// <returns></returns>
public static async Task<ManagedWorkspace> LoadOrCreateAsync(string hostName, DirectoryReference baseDir, bool bOverwrite, ILogger logger, CancellationToken cancellationToken)
{
if (Exists(baseDir))
{
try
{
return await LoadAsync(hostName, baseDir, logger, cancellationToken);
}
catch (Exception ex)
{
if (bOverwrite)
{
logger.LogWarning(ex, "Unable to load existing repository.");
}
else
{
throw;
}
}
}
return await CreateAsync(hostName, baseDir, logger, cancellationToken);
}
/*
public static PerforceConnection GetPerforceConnection(PerforceConnection Perforce)
{
if (Perforce.UserName == null || HostName == null)
{
InfoRecord ServerInfo = await Perforce.GetInfoAsync(InfoOptions.ShortOutput, CancellationToken);
if (Perforce.UserName == null)
{
Perforce = new PerforceConnection(Perforce) { UserName = ServerInfo.UserName };
}
if (HostName == null)
{
if (ServerInfo.ClientHost == null)
{
throw new Exception("Unable to determine host name");
}
else
{
HostName = ServerInfo.ClientHost;
}
}
}
return Perforce;
}
*/
/// <summary>
/// Creates a repository at the given location
/// </summary>
/// <param name="hostName">Name of the current machine.</param>
/// <param name="baseDir">The base directory for the repository</param>
/// <param name="logger">The log output device</param>
/// <param name="cancellationToken">Cancellation token for this operation</param>
/// <returns>New repository instance</returns>
public static async Task<ManagedWorkspace> CreateAsync(string hostName, DirectoryReference baseDir, ILogger logger, CancellationToken cancellationToken)
{
logger.LogInformation("Creating repository at {Location}...", baseDir);
// Make sure all the fields are valid
DirectoryReference.CreateDirectory(baseDir);
FileUtils.ForceDeleteDirectoryContents(baseDir);
ManagedWorkspace repo = new ManagedWorkspace(hostName, 1, baseDir, logger);
await repo.SaveAsync(TransactionState.Clean, cancellationToken);
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="hostName">Name of the current host. Will be obtained from a 'p4 info' call if not specified</param>
/// <param name="baseDir">The base directory for the repository</param>
/// <param name="logger">The log output device</param>
/// <param name="cancellationToken">Cancellation token for this command</param>
public static async Task<ManagedWorkspace> LoadAsync(string hostName, DirectoryReference baseDir, ILogger logger, CancellationToken cancellationToken)
{
if (!Exists(baseDir))
{
throw new FatalErrorException("No valid repository found at {0}", baseDir);
}
FileReference dataFile = FileReference.Combine(baseDir, DataFileName);
RestoreBackup(dataFile);
byte[] data = await FileReference.ReadAllBytesAsync(dataFile, cancellationToken);
MemoryReader reader = new MemoryReader(data.AsMemory());
int version = reader.ReadInt32();
if (version > CurrentVersion)
{
throw new FatalErrorException("Unsupported data format (version {0}, current {1})", version, CurrentVersion);
}
bool bRequiresRepair = reader.ReadBoolean();
uint nextSequenceNumber = reader.ReadUInt32();
ManagedWorkspace repo = new ManagedWorkspace(hostName, nextSequenceNumber, baseDir, logger);
repo._bRequiresRepair = bRequiresRepair;
int numTrackedFiles = reader.ReadInt32();
for (int idx = 0; idx < numTrackedFiles; idx++)
{
CachedFileInfo trackedFile = reader.ReadCachedFileInfo(repo._cacheDir);
repo._contentIdToTrackedFile.Add(trackedFile.ContentId, trackedFile);
repo._cacheEntries.Add(trackedFile.CacheId);
}
reader.ReadWorkspaceDirectoryInfo(repo._workspace, (ManagedWorkspaceVersion)version);
await repo.RunOptionalRepairAsync(cancellationToken);
return repo;
}
/// <summary>
/// Save the state of the repository
/// </summary>
private async Task SaveAsync(TransactionState state, CancellationToken cancellationToken)
{
// Allocate the buffer for writing
int serializedSize = sizeof(int) + sizeof(byte) + sizeof(int) + sizeof(int) + _contentIdToTrackedFile.Values.Sum(x => x.GetSerializedSize()) + _workspace.GetSerializedSize();
byte[] buffer = new byte[serializedSize];
// Write the data to memory
MemoryWriter writer = new MemoryWriter(buffer.AsMemory());
writer.WriteInt32(CurrentVersion);
writer.WriteBoolean(_bRequiresRepair || (state != TransactionState.Clean));
writer.WriteUInt32(_nextSequenceNumber);
writer.WriteInt32(_contentIdToTrackedFile.Count);
foreach (CachedFileInfo trackedFile in _contentIdToTrackedFile.Values)
{
writer.WriteCachedFileInfo(trackedFile);
}
writer.WriteWorkspaceDirectoryInfo(_workspace);
writer.CheckOffset(serializedSize);
// Write it to disk
FileReference dataFile = FileReference.Combine(_baseDir, DataFileName);
BeginTransaction(dataFile);
await FileReference.WriteAllBytesAsync(dataFile, buffer, cancellationToken);
CompleteTransaction(dataFile);
}
#region Commands
/// <summary>
/// Cleans the current workspace
/// </summary>
/// <param name="bRemoveUntracked">Whether to remove untracked files</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task CleanAsync(bool bRemoveUntracked, CancellationToken cancellationToken)
{
Stopwatch timer = Stopwatch.StartNew();
_logger.LogInformation("Cleaning workspace...");
using (_logger.BeginIndentScope(" "))
{
await CleanInternalAsync(bRemoveUntracked, cancellationToken);
}
_logger.LogInformation("Completed in {ElapsedTime}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Cleans the current workspace
/// </summary>
/// <param name="bRemoveUntracked">Whether to remove untracked files</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task CleanInternalAsync(bool bRemoveUntracked, CancellationToken cancellationToken)
{
FileInfo[] filesToDelete;
DirectoryInfo[] directoriesToDelete;
using (Trace("FindFilesToClean"))
using (ILoggerProgress status = _logger.BeginProgressScope("Finding files to clean..."))
{
Stopwatch timer = Stopwatch.StartNew();
_workspace.Refresh(bRemoveUntracked, out filesToDelete, out directoriesToDelete);
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
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, '/'))));
const int MaxDisplay = 1000;
foreach (string path in paths.OrderBy(x => x).Take(MaxDisplay))
{
_logger.LogInformation($" {path}");
}
if (paths.Count > MaxDisplay)
{
_logger.LogInformation(" +{NumPaths:n0} more", paths.Count - MaxDisplay);
}
using (Trace("CleanFiles"))
using (ILoggerProgress scope = _logger.BeginProgressScope("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.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
await SaveAsync(TransactionState.Clean, cancellationToken);
}
}
/// <summary>
/// Empties the staging directory of any staged files
/// </summary>
public async Task ClearAsync(CancellationToken cancellationToken)
{
Stopwatch timer = Stopwatch.StartNew();
_logger.LogInformation("Clearing workspace...");
using (Trace("Clear"))
using (_logger.BeginIndentScope(" "))
{
await CleanInternalAsync(true, cancellationToken);
await RemoveFilesFromWorkspaceAsync(StreamSnapshot.Empty, cancellationToken);
await SaveAsync(TransactionState.Clean, cancellationToken);
}
_logger.LogInformation("Completed in {ElapsedTime}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Dumps the contents of the repository to the log for analysis
/// </summary>
public void Dump()
{
Stopwatch timer = Stopwatch.StartNew();
_logger.LogInformation("Dumping repository to log...");
WorkspaceFileInfo[] workspaceFiles = _workspace.GetFiles().OrderBy(x => x.GetLocation().FullName).ToArray();
if (workspaceFiles.Length > 0)
{
_logger.LogDebug(" Workspace:");
foreach (WorkspaceFileInfo file in workspaceFiles)
{
_logger.LogDebug(" {File,-128} [{ContentId,-48}] [{Length,20:n0}] [{LastModified,20}]{Writable}", file.GetClientPath(), file.ContentId, file._length, file._lastModifiedTicks, file._bReadOnly ? "" : " [ writable ]");
}
}
if (_contentIdToTrackedFile.Count > 0)
{
_logger.LogDebug(" Cache:");
foreach (KeyValuePair<FileContentId, CachedFileInfo> pair in _contentIdToTrackedFile)
{
_logger.LogDebug(" {File,-128} [{ContentId,-48}] [{Length,20:n0}] [{LastModified,20}]{Writable}", pair.Value.GetLocation(), pair.Key, pair.Value.Length, pair.Value.LastModifiedTicks, pair.Value.BReadOnly ? "" : "[ writable ]");
}
}
_logger.LogInformation("Completed in {ElapsedTime}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Checks the integrity of the cache
/// </summary>
public async Task RepairAsync(CancellationToken cancellationToken)
{
using (Trace("Repair"))
using (ILoggerProgress status = _logger.BeginProgressScope("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<CachedFileInfo> trackedFiles = _contentIdToTrackedFile.Values.ToList();
foreach (CachedFileInfo trackedFile in trackedFiles)
{
if (!trackedFile.CheckIntegrity(_logger))
{
RemoveTrackedFile(trackedFile);
}
}
// Clear the repair flag
_bRequiresRepair = false;
await SaveAsync(TransactionState.Clean, cancellationToken);
status.Progress = "Done";
}
}
/// <summary>
/// Cleans the current workspace
/// </summary>
public async Task RevertAsync(IPerforceConnection perforceClient, CancellationToken cancellationToken)
{
Stopwatch timer = Stopwatch.StartNew();
await RevertInternalAsync(perforceClient, cancellationToken);
_logger.LogInformation("Completed in {ElapsedTime}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Checks the bRequiresRepair flag, and repairs/resets it if set.
/// </summary>
private async Task RunOptionalRepairAsync(CancellationToken cancellationToken)
{
if (_bRequiresRepair)
{
await RepairAsync(cancellationToken);
}
}
/// <summary>
/// Shrink the size of the cache to the given size
/// </summary>
/// <param name="maxSize">The maximum cache size, in bytes</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task PurgeAsync(long maxSize, CancellationToken cancellationToken)
{
_logger.LogInformation("Purging cache (limit {MaxSize:n0} bytes)...", maxSize);
using (Trace("Purge"))
using (_logger.BeginIndentScope(" "))
{
List<CachedFileInfo> cachedFiles = _contentIdToTrackedFile.Values.OrderBy(x => x.SequenceNumber).ToList();
int numRemovedFiles = 0;
long totalSize = cachedFiles.Sum(x => x.Length);
while (maxSize < totalSize && numRemovedFiles < cachedFiles.Count)
{
CachedFileInfo file = cachedFiles[numRemovedFiles];
RemoveTrackedFile(file);
totalSize -= file.Length;
numRemovedFiles++;
}
await SaveAsync(TransactionState.Clean, cancellationToken);
_logger.LogInformation("{NumFilesRemoved} files removed, {NumFilesRemaining} files remaining, new size {NewSize:n0} bytes.", numRemovedFiles, cachedFiles.Count - numRemovedFiles, totalSize);
}
}
/// <summary>
/// Configures the client for the given stream
/// </summary>
/// <param name="perforceClient">The Perforce connection</param>
/// <param name="streamName">Name of the stream</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task SetupAsync(IPerforceConnection perforceClient, string streamName, CancellationToken cancellationToken)
{
await UpdateClientAsync(perforceClient, streamName, cancellationToken);
}
/// <summary>
/// Prints stats showing coherence between different streams
/// </summary>
public async Task StatsAsync(IPerforceConnection perforceClient, List<string> streamNames, List<string> view, CancellationToken cancellationToken)
{
_logger.LogInformation("Finding stats for {NumStreams} streams", streamNames.Count);
using (_logger.BeginIndentScope(" "))
{
// Update the list of files in each stream
Tuple<int, StreamSnapshot>[] streamState = new Tuple<int, StreamSnapshot>[streamNames.Count];
for (int idx = 0; idx < streamNames.Count; idx++)
{
string streamName = streamNames[idx];
_logger.LogInformation("Finding contents of {StreamName}:", streamName);
using (_logger.BeginIndentScope(" "))
{
_createdClients.Remove(perforceClient.Settings.ClientName!); // Force the client to be updated
await UpdateClientAsync(perforceClient, streamName, cancellationToken);
int changeNumber = await GetLatestClientChangeAsync(perforceClient, cancellationToken);
_logger.LogInformation("Latest change is CL {ChangeNumber}", changeNumber);
await RevertInternalAsync(perforceClient, cancellationToken);
await ClearClientHaveTableAsync(perforceClient, cancellationToken);
await UpdateClientHaveTableAsync(perforceClient, changeNumber, view, cancellationToken);
StreamSnapshot contents = await FindClientContentsAsync(perforceClient, changeNumber, cancellationToken);
streamState[idx] = Tuple.Create(changeNumber, contents);
GC.Collect();
}
}
// Find stats for
using (Trace("Stats"))
using (ILoggerProgress scope = _logger.BeginProgressScope("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<StreamFile> 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<StreamFile> 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
_logger.LogInformation("");
_logger.LogInformation("Each row shows the size of files in a stream which are unique to that stream compared to each column:");
_logger.LogInformation("");
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(" | ");
}
_logger.LogInformation(row.ToString());
}
_logger.LogInformation("");
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
}
/// <summary>
/// Prints information about the repository state
/// </summary>
public void Status()
{
// Print size stats
_logger.LogInformation("Cache contains {NumFiles:n0} files, {TotalSize:n1}mb", _contentIdToTrackedFile.Count, _contentIdToTrackedFile.Values.Sum(x => x.Length) / (1024.0 * 1024.0));
_logger.LogInformation("Stage contains {NumFiles:n0} files, {TotalSize: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)
{
_logger.LogInformation("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();
}
_logger.LogInformation(" {0}", difference);
}
Console.ResetColor();
}
}
/// <summary>
/// Switches to the given stream
/// </summary>
/// <param name="perforce">The perforce connection</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="view">View of the workspace</param>
/// <param name="bRemoveUntracked">Whether to remove untracked files from 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>
/// <param name="cancellationToken">Cancellation token</param>
public async Task SyncAsync(IPerforceConnection perforce, string streamName, int changeNumber, IReadOnlyList<string> view, bool bRemoveUntracked, bool bFakeSync, FileReference? cacheFile, CancellationToken cancellationToken)
{
Stopwatch timer = Stopwatch.StartNew();
if (changeNumber == -1)
{
_logger.LogInformation("Syncing to {StreamName} at latest", streamName);
}
else
{
_logger.LogInformation("Syncing to {StreamName} at CL {CL}", streamName, changeNumber);
}
using (_logger.BeginIndentScope(" "))
{
// Update the client to the current stream
await UpdateClientAsync(perforce, streamName, cancellationToken);
// Get the latest change number
if (changeNumber == -1)
{
changeNumber = await GetLatestClientChangeAsync(perforce, cancellationToken);
}
// Revert any open files
await RevertInternalAsync(perforce, cancellationToken);
// Force the P4 metadata to match up
Task updateHaveTableTask = Task.Run(() => UpdateClientHaveTableAsync(perforce, changeNumber, view, cancellationToken), cancellationToken);
// Clean the current workspace
await CleanInternalAsync(bRemoveUntracked, cancellationToken);
// Wait for the have table update to finish
await updateHaveTableTask;
// Update the state of the current stream, if necessary
StreamSnapshot? contents;
if (cacheFile == null)
{
contents = await FindClientContentsAsync(perforce, changeNumber, cancellationToken);
}
else
{
contents = await TryLoadClientContentsAsync(cacheFile, streamName, cancellationToken);
if (contents == null)
{
contents = await FindAndSaveClientContentsAsync(perforce, streamName, changeNumber, cacheFile, cancellationToken);
}
}
// Sync all the appropriate files
await RemoveFilesFromWorkspaceAsync(contents, cancellationToken);
await AddFilesToWorkspaceAsync(perforce, contents, bFakeSync, cancellationToken);
}
_logger.LogInformation("Completed in {ElapsedTime}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Replays the effects of unshelving a changelist, but clobbering files in the workspace rather than actually unshelving them (to prevent problems with multiple machines locking them)
/// </summary>
/// <returns>Async task</returns>
public async Task UnshelveAsync(IPerforceConnection perforce, int unshelveChangelist, CancellationToken cancellationToken)
{
// Need to mark those files as dirty - update the workspace with those files
// Delete is fine, but need to flag anything added
Stopwatch timer = Stopwatch.StartNew();
_logger.LogInformation("Unshelving changelist {Change}...", unshelveChangelist);
// query the contents of the shelved changelist
List<DescribeRecord> records = await perforce.DescribeAsync(DescribeOptions.Shelved, -1, new int[] { unshelveChangelist }, cancellationToken);
if (records.Count != 1)
{
throw new PerforceException($"Changelist {unshelveChangelist} is not shelved");
}
DescribeRecord lastRecord = records[0];
if (lastRecord.Files.Count == 0)
{
throw new PerforceException($"Changelist {unshelveChangelist} does not contain any shelved files");
}
// query the location of each file
List<PerforceResponse<WhereRecord>> whereResponseList = await perforce.TryWhereAsync(lastRecord.Files.Select(x => x.DepotFile).ToArray(), cancellationToken).ToListAsync(cancellationToken);
List<WhereRecord> whereRecords = whereResponseList.Where(x => x.Succeeded).Select(x => x.Data).ToList();
// parse out all the list of deleted and modified files
List<WhereRecord> deleteFiles = new List<WhereRecord>();
List<WhereRecord> writeFiles = new List<WhereRecord>();
foreach (DescribeFileRecord fileRecord in lastRecord.Files)
{
WhereRecord? whereRecord = whereRecords.FirstOrDefault(x => x.DepotFile.Equals(fileRecord.DepotFile, StringComparison.OrdinalIgnoreCase));
if (whereRecord == null)
{
_logger.LogInformation("Unable to get location of {File} in current workspace; ignoring.", fileRecord.DepotFile);
continue;
}
switch (fileRecord.Action)
{
case FileAction.Delete:
case FileAction.MoveDelete:
deleteFiles.Add(whereRecord);
break;
case FileAction.Add:
case FileAction.Edit:
case FileAction.MoveAdd:
case FileAction.Branch:
case FileAction.Integrate:
writeFiles.Add(whereRecord);
break;
default:
throw new Exception($"Unknown action '{fileRecord.Action}' for shelved file {fileRecord.DepotFile}");
}
}
// Add all the files to be written to the workspace with invalid metadata. This will ensure they're removed on next clean.
if (writeFiles.Count > 0)
{
_logger.LogInformation("Removing {NumFiles} files from tracked workspace", writeFiles.Count);
foreach (WhereRecord writeFile in writeFiles)
{
string path = Regex.Replace(writeFile.ClientFile, "^//[^/]+/", "");
_workspace.AddFile(new Utf8String(path), 0, 0, false, new FileContentId(Md5Hash.Zero, default));
}
await SaveAsync(TransactionState.Clean, CancellationToken.None);
}
// Delete all the files
foreach (WhereRecord deleteFile in deleteFiles)
{
string localPath = deleteFile.Path;
if (File.Exists(localPath))
{
_logger.LogInformation(" Deleting {LocalPath}", localPath);
FileUtils.ForceDeleteFile(localPath);
}
}
// Add all the new files
foreach (WhereRecord writeFile in writeFiles)
{
string localPath = writeFile.Path;
_logger.LogInformation(" Writing {LocalPath}", localPath);
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
PerforceResponse printResponse = await perforce.TryPrintAsync(localPath, $"{writeFile}@={unshelveChangelist}", cancellationToken);
if (!printResponse.Succeeded)
{
_logger.LogWarning("Unable to print {LocalPath}: {Error}", localPath, printResponse.ToString());
}
}
_logger.LogInformation("Completed in {TimeSeconds}s", $"{timer.Elapsed.TotalSeconds:0.0}");
}
/// <summary>
/// Populates the cache with the head revision of the given streams.
/// </summary>
public async Task PopulateAsync(List<PopulateRequest> requests, bool bFakeSync, CancellationToken cancellationToken)
{
_logger.LogInformation("Populating with {NumStreams} streams", requests.Count);
using (_logger.BeginIndentScope(" "))
{
Tuple<int, StreamSnapshot>[] streamState = await PopulateCleanAsync(requests, cancellationToken);
await PopulateSyncAsync(requests, streamState, bFakeSync, cancellationToken);
}
}
/// <summary>
/// Perform the clean part of a populate command
/// </summary>
public async Task<Tuple<int, StreamSnapshot>[]> PopulateCleanAsync(List<PopulateRequest> requests, CancellationToken cancellationToken)
{
// Revert all changes in each of the unique clients
foreach (PopulateRequest request in requests)
{
using IPerforceConnection perforce = await request._perforceClient.WithoutClientAsync();
PerforceResponse<ClientRecord> response = await perforce.TryGetClientAsync(request._perforceClient.Settings.ClientName!, cancellationToken);
if (response.Succeeded)
{
await RevertInternalAsync(request._perforceClient, cancellationToken);
}
}
// Clean the current workspace
await CleanAsync(true, cancellationToken);
// Update the list of files in each stream
Tuple<int, StreamSnapshot>[] streamState = new Tuple<int, StreamSnapshot>[requests.Count];
for (int idx = 0; idx < requests.Count; idx++)
{
PopulateRequest request = requests[idx];
string streamName = request._streamName;
_logger.LogInformation("Finding contents of {StreamName}:", streamName);
using (_logger.BeginIndentScope(" "))
{
await DeleteClientAsync(request._perforceClient, cancellationToken);
await UpdateClientAsync(request._perforceClient, streamName, cancellationToken);
int changeNumber = await GetLatestClientChangeAsync(request._perforceClient, cancellationToken);
_logger.LogInformation("Latest change is CL {CL}", changeNumber);
await UpdateClientHaveTableAsync(request._perforceClient, changeNumber, request._view, cancellationToken);
StreamSnapshot contents = await FindClientContentsAsync(request._perforceClient, changeNumber, cancellationToken);
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 (requests.Count > 0)
{
await RemoveFilesFromWorkspaceAsync(streamState[0].Item2, cancellationToken);
}
// Shrink the contents of the cache
using (Trace("UpdateCache"))
using (ILoggerProgress status = _logger.BeginProgressScope("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 < requests.Count; idx++)
{
List<StreamFile> files = streamState[idx].Item2.GetFiles();
foreach (StreamFile 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<CachedFileInfo> trackedFiles = _contentIdToTrackedFile.Values.ToList();
foreach (CachedFileInfo trackedFile in trackedFiles)
{
if (!contentIdToLength.ContainsKey(trackedFile.ContentId))
{
RemoveTrackedFile(trackedFile);
}
}
GC.Collect();
double totalSize = contentIdToLength.Sum(x => x.Value) / (1024.0 * 1024.0);
status.Progress = String.Format("{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);
}
return streamState;
}
/// <summary>
/// Perform the sync part of a populate command
/// </summary>
public async Task PopulateSyncAsync(List<PopulateRequest> requests, Tuple<int, StreamSnapshot>[] streamState, bool bFakeSync, CancellationToken cancellationToken)
{
// Sync all the new files
for (int idx = 0; idx < requests.Count; idx++)
{
PopulateRequest request = requests[idx];
string streamName = request._streamName;
_logger.LogInformation("Syncing files for {StreamName}:", streamName);
using (_logger.BeginIndentScope(" "))
{
await DeleteClientAsync(request._perforceClient, cancellationToken);
await UpdateClientAsync(request._perforceClient, streamName, cancellationToken);
int changeNumber = streamState[idx].Item1;
await UpdateClientHaveTableAsync(request._perforceClient, changeNumber, requests[idx]._view, cancellationToken);
StreamSnapshot contents = streamState[idx].Item2;
await RemoveFilesFromWorkspaceAsync(contents, cancellationToken);
await AddFilesToWorkspaceAsync(request._perforceClient, contents, bFakeSync, cancellationToken);
}
}
// Save the new repo state
await SaveAsync(TransactionState.Clean, cancellationToken);
}
#endregion
#region Core operations
/// <summary>
/// Deletes a client
/// </summary>
/// <param name="perforceClient">The Perforce connection</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Async task</returns>
public async Task DeleteClientAsync(IPerforceConnection perforceClient, CancellationToken cancellationToken)
{
PerforceResponse response = await perforceClient.TryDeleteClientAsync(DeleteClientOptions.None, perforceClient.Settings.ClientName!, cancellationToken);
if (response.Error != null && response.Error.Generic != PerforceGenericCode.Unknown)
{
if (response.Error.Generic == PerforceGenericCode.NotYet)
{
await RevertInternalAsync(perforceClient, cancellationToken);
response = await perforceClient.TryDeleteClientAsync(DeleteClientOptions.None, perforceClient.Settings.ClientName!, cancellationToken);
}
response.EnsureSuccess();
}
_createdClients.Remove(perforceClient.Settings.ClientName!);
}
/// <summary>
/// Sets the stream for the current client
/// </summary>
/// <param name="perforceClient">The Perforce connection</param>
/// <param name="streamName">New stream for the client</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task UpdateClientAsync(IPerforceConnection perforceClient, string streamName, CancellationToken cancellationToken)
{
// Create or update the client if it doesn't exist already
ClientRecord? client;
if (!_createdClients.TryGetValue(perforceClient.Settings.ClientName!, out client) || client.Stream != streamName)
{
using (Trace("UpdateClient"))
using (ILoggerProgress status = _logger.BeginProgressScope("Updating client..."))
{
Stopwatch timer = Stopwatch.StartNew();
client = new ClientRecord(perforceClient.Settings.ClientName!, perforceClient.Settings.UserName!, _workspaceDir.FullName);
client.Host = _hostName;
client.Stream = streamName;
client.Type = "partitioned";
using IPerforceConnection perforce = await perforceClient.WithoutClientAsync();
PerforceResponse response = await perforce.TryCreateClientAsync(client, cancellationToken);
if (!response.Succeeded)
{
await perforceClient.TryDeleteClientAsync(DeleteClientOptions.None, perforceClient.Settings.ClientName!, cancellationToken);
await perforceClient.CreateClientAsync(client, cancellationToken);
}
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
_createdClients[perforceClient.Settings.ClientName!] = client;
}
// 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("P4PORT={0}", perforceClient.Settings.ServerAndPort);
writer.WriteLine("P4CLIENT={0}", perforceClient.Settings.ClientName);
}
}
/// <summary>
/// Gets the latest change submitted for the given stream
/// </summary>
/// <param name="perforceClient">The Perforce connection</param>
/// <param name="streamName">The stream to sync</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The latest changelist number</returns>
public async Task<int> GetLatestChangeAsync(IPerforceConnection perforceClient, string streamName, CancellationToken cancellationToken)
{
// Update the client to the current stream
await UpdateClientAsync(perforceClient, streamName, cancellationToken);
// Get the latest change number
return await GetLatestClientChangeAsync(perforceClient, cancellationToken);
}
/// <summary>
/// Get the latest change number in the current client
/// </summary>
/// <param name="perforceClient">The perforce client connection</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The latest submitted change number</returns>
private async Task<int> GetLatestClientChangeAsync(IPerforceConnection perforceClient, CancellationToken cancellationToken)
{
int changeNumber;
using (Trace("FindChange"))
using (ILoggerProgress status = _logger.BeginProgressScope("Finding latest change..."))
{
Stopwatch timer = Stopwatch.StartNew();
List<ChangesRecord> changes = await perforceClient.GetChangesAsync(ChangesOptions.None, 1, ChangeStatus.Submitted, new[] { String.Format("//{0}/...", perforceClient.Settings.ClientName) }, cancellationToken);
changeNumber = changes[0].Number;
status.Progress = String.Format("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="perforceClient">The current client connection</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task RevertInternalAsync(IPerforceConnection perforceClient, CancellationToken cancellationToken)
{
using (Trace("Revert"))
using (ILoggerProgress status = _logger.BeginProgressScope("Reverting changes..."))
{
Stopwatch timer = Stopwatch.StartNew();
// Get a list of open files
List<OpenedRecord> openedFilesResponse = await perforceClient.OpenedAsync(OpenedOptions.ShortOutput, -1, perforceClient.Settings.ClientName!, null, 1, FileSpecList.Any, cancellationToken).ToListAsync(cancellationToken);
// If there are any files, revert them
if (openedFilesResponse.Any())
{
await perforceClient.RevertAsync(-1, null, RevertOptions.KeepWorkspaceFiles, new[] { "//..." }, cancellationToken);
}
// Find all the open changes
List<ChangesRecord> changes = await perforceClient.GetChangesAsync(ChangesOptions.None, perforceClient.Settings.ClientName!, -1, ChangeStatus.Pending, null, new string[0], cancellationToken);
// Delete the changelist
foreach (ChangesRecord change in changes)
{
// Delete the shelved files
List<DescribeRecord> describeResponse = await perforceClient.DescribeAsync(DescribeOptions.Shelved, -1, new[] { change.Number }, cancellationToken);
foreach (DescribeRecord record in describeResponse)
{
if (record.Files.Count > 0)
{
await perforceClient.DeleteShelvedFilesAsync(record.Number, new string[0], cancellationToken);
}
}
// Delete the changelist
await perforceClient.DeleteChangeAsync(DeleteChangeOptions.None, change.Number, cancellationToken);
}
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
/// <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="perforceClient">The client connection</param>
/// <param name="cancellationToken">The cancellation token</param>
private async Task ClearClientHaveTableAsync(IPerforceConnection perforceClient, CancellationToken cancellationToken)
{
using (Trace("ClearHaveTable"))
using (ILoggerProgress scope = _logger.BeginProgressScope("Clearing have table..."))
{
Stopwatch timer = Stopwatch.StartNew();
await perforceClient.SyncQuietAsync(SyncOptions.KeepWorkspaceFiles, -1, new[] { String.Format("//{0}/...#0", perforceClient.Settings.ClientName!) }, cancellationToken);
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
/// <summary>
/// Updates the have table to reflect the given stream
/// </summary>
/// <param name="perforceClient">The client connection</param>
/// <param name="changeNumber">The change number to sync. May be -1, for latest.</param>
/// <param name="view">View of the stream. Each entry should be a path relative to the stream root, with an optional '-'prefix.</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task UpdateClientHaveTableAsync(IPerforceConnection perforceClient, int changeNumber, IReadOnlyList<string> view, CancellationToken cancellationToken)
{
using (Trace("UpdateHaveTable"))
using (ILoggerProgress scope = _logger.BeginProgressScope("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 (view.Count == 0 || view[0].StartsWith("-"))
{
await UpdateHaveTablePathAsync(perforceClient, $"//{perforceClient.Settings.ClientName}/...@{changeNumber}", cancellationToken);
}
else
{
await UpdateHaveTablePathAsync(perforceClient, $"//{perforceClient.Settings.ClientName}/...#0", cancellationToken);
}
// Update with the contents of each filter
foreach (string filter in view)
{
string syncPath;
if (filter.StartsWith("-"))
{
syncPath = String.Format("//{0}/{1}#0", perforceClient.Settings.ClientName, RemoveLeadingSlash(filter.Substring(1)));
}
else
{
syncPath = String.Format("//{0}/{1}@{2}", perforceClient.Settings.ClientName, RemoveLeadingSlash(filter), changeNumber);
}
await UpdateHaveTablePathAsync(perforceClient, syncPath, cancellationToken);
}
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
/// <summary>
/// Update a path in the have table
/// </summary>
/// <param name="perforceClient">The Perforce client</param>
/// <param name="syncPath">Path to sync</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Async task</returns>
private async Task UpdateHaveTablePathAsync(IPerforceConnection perforceClient, string syncPath, CancellationToken cancellationToken)
{
PerforceResponseList<SyncSummaryRecord> responseList = await perforceClient.TrySyncQuietAsync(SyncOptions.KeepWorkspaceFiles, -1, new[] { syncPath }, cancellationToken);
foreach (PerforceResponse<SyncSummaryRecord> response in responseList)
{
PerforceError? error = response.Error;
if (error != null && error.Generic != PerforceGenericCode.Empty)
{
throw new PerforceException(error);
}
}
}
/// <summary>
/// Optimized record definition for fstat calls when populating a workspace. Since there are so many files in a typical branch,
/// the speed of serializing these records is crucial for performance. Rather than deseralizing everything, we filter to just
/// the fields we need, and avoid any unnecessary conversions from their primitive data types.
/// </summary>
class FStatIndexedRecord
{
// Note: This enum is used for indexing an array of fields, and member names much match P4 field names (including case).
enum Field
{
code,
depotFile,
clientFile,
headType,
haveRev,
fileSize,
digest
}
public static readonly string[] FieldNames = Enum.GetNames(typeof(Field));
public static readonly Utf8String[] Utf8FieldNames = Array.ConvertAll(FieldNames, x => new Utf8String(x));
public PerforceValue[] Values { get; } = new PerforceValue[FieldNames.Length];
public Utf8String DepotFile => Values[(int)Field.depotFile].GetString();
public Utf8String ClientFile => Values[(int)Field.clientFile].GetString();
public Utf8String HeadType => Values[(int)Field.headType].GetString();
public Utf8String HaveRev => Values[(int)Field.haveRev].GetString();
public long FileSize => Values[(int)Field.fileSize].AsLong();
public Utf8String Digest => Values[(int)Field.digest].GetString();
}
/// <summary>
/// Get the contents of the client, as synced.
/// </summary>
/// <param name="perforceClient">The client connection</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="cancellationToken">Cancellation token</param>
private async Task<StreamSnapshotFromMemory> FindClientContentsAsync(IPerforceConnection perforceClient, int changeNumber, CancellationToken cancellationToken)
{
StreamTreeBuilder builder = new StreamTreeBuilder();
using (Trace("FetchMetadata"))
using (ILoggerProgress scope = _logger.BeginProgressScope("Fetching metadata..."))
{
Stopwatch timer = Stopwatch.StartNew();
// Get the expected prefix for any paths in client syntax
Utf8String clientPrefix = $"//{perforceClient.Settings.ClientName}/";
// List of the last path fragments. Since file records that are returned are typically sorted by their position in the tree, we can save quite a lot of processing by
// reusing as many fragemnts as possible.
List<(Utf8String, StreamTreeBuilder)> fragments = new List<(Utf8String, StreamTreeBuilder)>();
// Handler for each returned record
FStatIndexedRecord record = new FStatIndexedRecord();
void HandleRecord(PerforceRecord rawRecord)
{
// Copy into the values array
rawRecord.CopyInto(FStatIndexedRecord.Utf8FieldNames, record.Values);
// Make sure it has all the fields we're interested in
if (record.Digest.IsEmpty)
{
return;
}
if (record.ClientFile.IsEmpty)
{
throw new InvalidDataException("Record returned by Peforce does not have ClientFile set");
}
if (!record.ClientFile.StartsWith(clientPrefix))
{
throw new InvalidDataException($"Client path returned by Perforce ('{record.ClientFile}') does not begin with client name ('{clientPrefix}')");
}
// Duplicate the client path. If we reference into the raw record, we'll prevent all the raw P4 output from being garbage collected.
Utf8String clientFile = record.ClientFile.Clone();
// Get the client path after the initial client prefix
ReadOnlySpan<byte> pathSpan = clientFile.Span;
// Parse out the data
StreamTreeBuilder lastStreamDirectory = builder;
// Try to match up as many fragments from the last file.
int fragmentMinIdx = clientPrefix.Length;
for (int fragmentIdx = 0; ; fragmentIdx++)
{
// Find the next directory separator
int fragmentMaxIdx = fragmentMinIdx;
while (fragmentMaxIdx < pathSpan.Length && pathSpan[fragmentMaxIdx] != '/')
{
fragmentMaxIdx++;
}
if (fragmentMaxIdx == pathSpan.Length)
{
fragments.RemoveRange(fragmentIdx, fragments.Count - fragmentIdx);
break;
}
// Get the fragment text
Utf8String fragment = new Utf8String(clientFile.Memory.Slice(fragmentMinIdx, fragmentMaxIdx - fragmentMinIdx));
// If this fragment matches the same fragment from the previous iteration, take the last stream directory straight away
if (fragmentIdx < fragments.Count)
{
if (fragments[fragmentIdx].Item1 == fragment)
{
lastStreamDirectory = fragments[fragmentIdx].Item2;
}
else
{
fragments.RemoveRange(fragmentIdx, fragments.Count - fragmentIdx);
}
}
// Otherwise, find or add a directory for this fragment into the last directory
if (fragmentIdx >= fragments.Count)
{
Utf8String unescapedFragment = PerforceUtils.UnescapePath(fragment);
StreamTreeBuilder? nextStreamDirectory;
if (!lastStreamDirectory.NameToTreeBuilder.TryGetValue(unescapedFragment, out nextStreamDirectory))
{
nextStreamDirectory = new StreamTreeBuilder();
lastStreamDirectory.NameToTreeBuilder.Add(unescapedFragment, nextStreamDirectory);
}
lastStreamDirectory = nextStreamDirectory;
fragments.Add((fragment, lastStreamDirectory));
}
// Move to the next fragment
fragmentMinIdx = fragmentMaxIdx + 1;
}
Md5Hash digest = Md5Hash.Parse(record.Digest);
FileContentId contentId = new FileContentId(digest, record.HeadType.Clone());
int revision = (int)Utf8String.ParseUnsignedInt(record.HaveRev);
// Add a new StreamFileInfo to the last directory object
Utf8String fileName = PerforceUtils.UnescapePath(clientFile.Slice(fragmentMinIdx));
lastStreamDirectory.NameToFile.Add(fileName, new StreamFile(record.DepotFile.Clone(), record.FileSize, contentId, revision));
}
// Create the workspace, and add records for all the files. Exclude deleted files with digest = null.
List<string> arguments = new List<string>();
arguments.Add("-Ol");
arguments.Add("-Op");
arguments.Add("-Os");
arguments.Add("-Rh");
arguments.Add("-T");
arguments.Add(String.Join(",", FStatIndexedRecord.FieldNames));
arguments.Add($"//{perforceClient.Settings.ClientName}/...@{changeNumber}");
await perforceClient.RecordCommandAsync("fstat", arguments, null, HandleRecord, cancellationToken);
// Output the elapsed time
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
return new StreamSnapshotFromMemory(builder);
}
/// <summary>
/// Loads the contents of a client from disk
/// </summary>
/// <param name="cacheFile">The cache file to read from</param>
/// <param name="basePath">Default path for the stream</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Contents of the workspace</returns>
async Task<StreamSnapshot?> TryLoadClientContentsAsync(FileReference cacheFile, Utf8String basePath, CancellationToken cancellationToken)
{
StreamSnapshot? contents = null;
if (FileReference.Exists(cacheFile))
{
using (Trace("ReadMetadata"))
using (ILoggerProgress scope = _logger.BeginProgressScope($"Reading cached metadata from {cacheFile}..."))
{
Stopwatch timer = Stopwatch.StartNew();
contents = await StreamSnapshotFromMemory.TryLoadAsync(cacheFile, basePath, cancellationToken);
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
return contents;
}
/// <summary>
/// Finds the contents of a workspace, and saves it to disk
/// </summary>
/// <param name="perforceClient">The client connection</param>
/// <param name="basePath">Base path for the stream</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="cacheFile">Location of the file to save the cached contents</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Contents of the workspace</returns>
private async Task<StreamSnapshotFromMemory> FindAndSaveClientContentsAsync(IPerforceConnection perforceClient, Utf8String basePath, int changeNumber, FileReference cacheFile, CancellationToken cancellationToken)
{
StreamSnapshotFromMemory contents = await FindClientContentsAsync(perforceClient, changeNumber, cancellationToken);
using (Trace("WriteMetadata"))
using (ILoggerProgress scope = _logger.BeginProgressScope($"Saving metadata to {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()));
await contents.Save(tempCacheFile, basePath);
// Try to move it into place
try
{
FileReference.Move(tempCacheFile, cacheFile);
}
catch (IOException)
{
if (!FileReference.Exists(cacheFile))
{
throw;
}
FileReference.Delete(tempCacheFile);
}
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
return contents;
}
/// <summary>
/// Remove files from the workspace
/// </summary>
/// <param name="contents">Contents of the target stream</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task RemoveFilesFromWorkspaceAsync(StreamSnapshot contents, CancellationToken cancellationToken)
{
// Make sure the repair flag is clear before we start
await RunOptionalRepairAsync(cancellationToken);
// Figure out what to remove
RemoveTransaction transaction;
using (Trace("GatherFilesToRemove"))
using (ILoggerProgress scope = _logger.BeginProgressScope("Gathering files to remove..."))
{
Stopwatch timer = Stopwatch.StartNew();
transaction = new RemoveTransaction(_workspace, contents, _contentIdToTrackedFile);
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
// Move files into the cache
KeyValuePair<FileContentId, WorkspaceFileInfo>[] filesToMove = transaction._filesToMove.ToArray();
if (filesToMove.Length > 0)
{
using (Trace("MoveToCache"))
using (ILoggerProgress scope = _logger.BeginProgressScope(String.Format("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);
CachedFileInfo newTrackingInfo = new CachedFileInfo(_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).
await SaveAsync(TransactionState.Dirty, cancellationToken);
// Execute all the moves and deletes
await ParallelTask.ForEachAsync(sourceAndTargetFiles, sourceAndTargetFile => FileUtils.ForceMoveFile(sourceAndTargetFile.Key, sourceAndTargetFile.Value));
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
// Remove files which are no longer needed
WorkspaceFileInfo[] filesToDelete = transaction._filesToDelete.ToArray();
if (filesToDelete.Length > 0)
{
using (Trace("DeleteFiles"))
using (ILoggerProgress scope = _logger.BeginProgressScope(String.Format("Deleting {0} {1}...", filesToDelete.Length, (filesToDelete.Length == 1) ? "file" : "files")))
{
Stopwatch timer = Stopwatch.StartNew();
await ParallelTask.ForEachAsync(filesToDelete, fileToDelete => RemoveFile(fileToDelete));
scope.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
// Remove directories which are no longer needed
WorkspaceDirectoryInfo[] directoriesToDelete = transaction._directoriesToDelete.ToArray();
if (directoriesToDelete.Length > 0)
{
using (Trace("DeleteDirectories"))
using (ILoggerProgress scope = _logger.BeginProgressScope(String.Format("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.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
// Update the workspace and save the new state
_workspace = transaction._newWorkspaceRootDir;
await SaveAsync(TransactionState.Clean, cancellationToken);
}
/// <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)
{
_logger.LogWarning(ex, "warning: Unable to delete file {FileName}.", fileToDelete.GetFullName());
_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)
{
_logger.LogWarning(ex, "warning: Unable to delete directory {0}", directoryToDelete);
_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>
/// <param name="cancellationToken">Cancellation token</param>
private async Task AddFilesToWorkspaceAsync(IPerforceConnection client, StreamSnapshot stream, bool bFakeSync, CancellationToken cancellationToken)
{
// Make sure the repair flag is reset
await RunOptionalRepairAsync(cancellationToken);
// Figure out what we need to do
AddTransaction transaction;
using (Trace("GatherFilesToAdd"))
using (ILoggerProgress status = _logger.BeginProgressScope("Gathering files to add..."))
{
Stopwatch timer = Stopwatch.StartNew();
transaction = new AddTransaction(_workspace, stream, _contentIdToTrackedFile);
_workspace = transaction._newWorkspaceRootDir;
await SaveAsync(TransactionState.Dirty, cancellationToken);
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
// Swap files in and out of the cache
WorkspaceFileToMove[] filesToMove = transaction._filesToMove.Values.ToArray();
if (filesToMove.Length > 0)
{
using (Trace("MoveFromCache"))
using (ILoggerProgress status = _logger.BeginProgressScope(String.Format("Moving {0} {1} from cache...", filesToMove.Length, (filesToMove.Length == 1) ? "file" : "files")))
{
Stopwatch timer = Stopwatch.StartNew();
await ParallelTask.ForEachAsync(filesToMove, fileToMove => MoveFileFromCache(fileToMove, transaction._filesToSync));
_contentIdToTrackedFile = _contentIdToTrackedFile.Where(x => !transaction._filesToMove.ContainsKey(x.Value)).ToDictionary(x => x.Key, x => x.Value);
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
// Swap files in and out of the cache
WorkspaceFileToCopy[] filesToCopy = transaction._filesToCopy.ToArray();
if (filesToCopy.Length > 0)
{
using (Trace("CopyFiles"))
using (ILoggerProgress status = _logger.BeginProgressScope(String.Format("Copying {0} {1} within workspace...", filesToCopy.Length, (filesToCopy.Length == 1) ? "file" : "files")))
{
Stopwatch timer = Stopwatch.StartNew();
await ParallelTask.ForEachAsync(filesToCopy, fileToCopy => CopyFileWithinWorkspace(fileToCopy, transaction._filesToSync));
status.Progress = $"({timer.Elapsed.TotalSeconds:0.0}s)";
}
}
// Find all the files we want to sync
WorkspaceFileToSync[] filesToSync = transaction._filesToSync.ToArray();
if (filesToSync.Length > 0)
{
long syncSize = filesToSync.Sum(x => x._streamFile.Length);
// Make sure there's enough space on this drive
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
long freeSpace = new DriveInfo(Path.GetPathRoot(_baseDir.FullName)).AvailableFreeSpace;
if (freeSpace - syncSize < MinScratchSpace)
{
throw new InsufficientSpaceException($"Not enough space to sync new files (free space: {freeSpace / (1024.0 * 1024.0):n1}mb, sync size: {syncSize / (1024.0 * 1024.0):n1}mb, min scratch space: {MinScratchSpace / (1024.0 * 1024.0):n1}mb)");
}
}
// Sync all the files
using (Trace("SyncFiles"))
using (ILoggerProgress status = _logger.BeginProgressScope(String.Format("Syncing {0} {1} using {2} threads...", filesToSync.Length, (filesToSync.Length == 1) ? "file" : "files", NumParallelSyncThreads)))
{
Stopwatch timer = Stopwatch.StartNew();
// Remove all the previous response files
foreach (FileReference file in DirectoryReference.EnumerateFiles(_baseDir, "SyncList-*.txt"))
{
FileUtils.ForceDeleteFile(file);
}
// Create a list of all the batches that we want to sync
List<(int, int)> batches = new List<(int, int)>();
for (int endIdx = 0; endIdx < filesToSync.Length;)
{
int beginIdx = endIdx;
// 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;
}
// Add this batch to the list
batches.Add((beginIdx, endIdx));
}
// The next batch to be synced
int nextBatchIdx = 0;
// Total size of synced files
long syncedSize = 0;
// Spawn some background threads to sync them
Dictionary<Task, int> tasks = new Dictionary<Task, int>();
try
{
while (tasks.Count > 0 || nextBatchIdx < batches.Count)
{
// Create new tasks
while (tasks.Count < NumParallelSyncThreads && nextBatchIdx < batches.Count)
{
(int batchBeginIdx, int batchEndIdx) = batches[nextBatchIdx];
Task task = Task.Run(() => SyncBatch(client, filesToSync, batchBeginIdx, batchEndIdx, bFakeSync, cancellationToken));
tasks[task] = nextBatchIdx++;
}
// Wait for anything to complete
Task completeTask = await Task.WhenAny(tasks.Keys);
await completeTask; // Make sure we re-throw any exceptions from the task that completed
int batchIdx = tasks[completeTask];
tasks.Remove(completeTask);
// Update metadata for the complete batch
(int beginIdx, int endIdx) = batches[batchIdx];
await ParallelTask.ForAsync(beginIdx, endIdx, idx => filesToSync[idx]._workspaceFile.UpdateMetadata());
// Save the current state every minute
TimeSpan elapsed = timer.Elapsed;
if (elapsed > TimeSpan.FromMinutes(5.0))
{
await SaveAsync(TransactionState.Dirty, cancellationToken);
_logger.LogInformation("Saved workspace state ({Elapsed:0.0}s)", (timer.Elapsed - elapsed).TotalSeconds);
timer.Restart();
}
// Update the status
for (int idx = beginIdx; idx < endIdx; idx++)
{
syncedSize += filesToSync[idx]._streamFile.Length;
}
status.Progress = String.Format("{0:n1}% ({1:n1}mb/{2:n1}mb)", syncedSize * 100.0 / syncSize, syncedSize / (1024.0 * 1024.0), syncSize / (1024.0 * 1024.0));
}
}
finally
{
await Task.WhenAll(tasks.Keys);
}
}
}
// Save the clean state
_workspace = transaction._newWorkspaceRootDir;
await SaveAsync(TransactionState.Clean, cancellationToken);
}
/// <summary>
/// Syncs a batch of files
/// </summary>
/// <param name="client">The client to sync</param>
/// <param name="filesToSync">List of files to sync</param>
/// <param name="beginIdx">First file to sync</param>
/// <param name="endIdx">Index of the last file to sync (exclusive)</param>
/// <param name="bFakeSync">Whether to fake a sync</param>
/// <param name="cancellationToken">Cancellation token for the request</param>
/// <returns>Async task</returns>
async Task SyncBatch(IPerforceConnection client, WorkspaceFileToSync[] filesToSync, int beginIdx, int endIdx, bool bFakeSync, CancellationToken cancellationToken)
{
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-{beginIdx}.txt");
using (StreamWriter writer = new StreamWriter(syncFileName.FullName))
{
for (int idx = beginIdx; idx < endIdx; idx++)
{
writer.WriteLine("{0}#{1}", filesToSync[idx]._streamFile.Path, filesToSync[idx]._streamFile.Revision);
}
}
using PerforceConnection clientWithFileList = new PerforceConnection(client.Settings, client.Logger);
clientWithFileList.GlobalOptions.Add($"-x\"{syncFileName}\"");
await clientWithFileList.SyncAsync(SyncOptions.Force | SyncOptions.FullDepotSyntax, -1, new string[0], cancellationToken).ToListAsync(cancellationToken);
}
}
/// <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, ConcurrentQueue<WorkspaceFileToSync> filesToSync)
{
try
{
FileReference.Move(fileToMove._trackedFile.GetLocation(), fileToMove._workspaceFile.GetLocation());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "warning: Unable to move {CacheFile} from cache to {WorkspaceFile}. Syncing instead.", fileToMove._trackedFile.GetLocation(), fileToMove._workspaceFile.GetLocation());
filesToSync.Enqueue(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, ConcurrentQueue<WorkspaceFileToSync> filesToSync)
{
try
{
FileReference.Copy(fileToCopy._sourceWorkspaceFile.GetLocation(), fileToCopy._targetWorkspaceFile.GetLocation());
fileToCopy._targetWorkspaceFile.UpdateMetadata();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "warning: Unable to copy {SourceFile} to {TargetFile}. Syncing instead.", fileToCopy._sourceWorkspaceFile.GetLocation(), fileToCopy._targetWorkspaceFile.GetLocation());
filesToSync.Enqueue(new WorkspaceFileToSync(fileToCopy._streamFile, fileToCopy._targetWorkspaceFile));
_bRequiresRepair = true;
}
}
void RemoveTrackedFile(CachedFileInfo 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.Span[idx];
}
while (!_cacheEntries.Add(cacheId))
{
cacheId++;
}
return cacheId;
}
/// <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);
}
}
/// <summary>
/// Creates a scoped trace object
/// </summary>
/// <param name="operation">Name of the operation</param>
/// <returns>Disposable object for the trace</returns>
private IDisposable Trace(string operation)
{
return TraceSpan.Create(operation, service: "hordeagent_repository");
}
#endregion
}
}