Files
2025-05-18 13:04:45 +08:00

953 lines
29 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Perforce;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
namespace UnrealGameSync
{
public sealed class UserErrorException : Exception
{
public int Code { get; }
public UserErrorException(string message, int code = 1) : base(message)
{
Code = code;
}
}
public class PerforceChangeDetails
{
public int Number { get; }
public string Description { get; }
public bool ContainsCode { get; }
public bool ContainsContent { get; }
public bool ContainsUgsConfig { get; }
public PerforceChangeDetails(DescribeRecord describeRecord, Func<string, bool>? isCodeFile = null)
{
isCodeFile ??= IsCodeFile;
Number = describeRecord.Number;
Description = describeRecord.Description;
// Check whether the files are code or content
foreach (DescribeFileRecord file in describeRecord.Files)
{
if (isCodeFile(file.DepotFile))
{
ContainsCode = true;
}
else
{
ContainsContent = true;
}
if (file.DepotFile.EndsWith("/UnrealGameSync.ini", StringComparison.OrdinalIgnoreCase))
{
ContainsUgsConfig = true;
}
}
}
public static bool IsCodeFile(string depotFile)
{
return PerforceUtils.CodeExtensions.Any(extension => depotFile.EndsWith(extension, StringComparison.OrdinalIgnoreCase));
}
}
public static class Utility
{
static readonly MemoryCache s_changeCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 4096 });
class CachedChangeRecord
{
public ChangesRecord ChangesRecord { get; }
public int? PrevNumber { get; set; }
public CachedChangeRecord(ChangesRecord changesRecord) => ChangesRecord = changesRecord;
}
static string GetChangeCacheKey(int number, Sha1Hash config) => $"change: {number} ({config})";
static string GetChangeDetailsCacheKey(int number, Sha1Hash config) => $"details: {number} ({config})";
static Sha1Hash GetConfigHash(IPerforceConnection perforce, IEnumerable<string> syncPaths, IEnumerable<string> codeRules)
{
StringBuilder digest = new StringBuilder();
digest.AppendLine($"Server: {perforce.Settings.ServerAndPort}");
digest.AppendLine($"User: {perforce.Settings.UserName}");
digest.AppendLine($"Client: {perforce.Settings.ClientName}");
foreach (string syncPath in syncPaths)
{
digest.AppendLine($"Sync: {syncPath}");
}
foreach (string codeRule in codeRules)
{
digest.AppendLine($"Rule: {codeRule}");
}
return Sha1Hash.Compute(Encoding.UTF8.GetBytes(digest.ToString()));
}
/// <summary>
/// Gets the code filter from the project config file
/// </summary>
public static string[] GetCodeFilter(ConfigFile projectConfigFile)
{
ConfigSection? projectConfigSection = projectConfigFile.FindSection("Perforce");
return projectConfigSection?.GetValues("CodeFilter", (string[]?)null) ?? Array.Empty<string>();
}
/// <summary>
/// Creates or returns cached <see cref="PerforceChangeDetails"/> objects describing the requested chagnes
/// </summary>
public static async IAsyncEnumerable<ChangesRecord> EnumerateChanges(IPerforceConnection perforce, IEnumerable<string> syncPaths, int? minChangeNumber, int? maxChangeNumber, int? maxChanges, [EnumeratorCancellation] CancellationToken cancellationToken)
{
CachedChangeRecord? prevCachedChangeRecord = null;
Sha1Hash configHash = GetConfigHash(perforce, syncPaths, Array.Empty<string>());
while (maxChanges == null || maxChanges.Value > 0)
{
// If we have a maximum changelist number, see if the previous change is in the cache
if (maxChangeNumber != null)
{
if (minChangeNumber != null && maxChangeNumber.Value < minChangeNumber.Value)
{
yield break;
}
string cacheKey = GetChangeCacheKey(maxChangeNumber.Value, configHash);
if (s_changeCache.TryGetValue(cacheKey, out CachedChangeRecord? change) && change != null)
{
yield return change.ChangesRecord;
if (maxChanges != null)
{
maxChanges = maxChanges.Value - 1;
}
if (change.PrevNumber != null)
{
maxChangeNumber = change.PrevNumber.Value;
}
else
{
maxChangeNumber = change.ChangesRecord.Number - 1;
}
prevCachedChangeRecord = change;
continue;
}
}
// Get the search range
string range;
if (minChangeNumber == null)
{
if (maxChangeNumber == null)
{
range = "";
}
else
{
range = $"@<={maxChangeNumber.Value}";
}
}
else
{
if (maxChangeNumber == null)
{
range = $"@{minChangeNumber.Value},#head";
}
else
{
range = $"@{minChangeNumber.Value},{maxChangeNumber.Value}";
}
}
// Get the next batch of change numbers
int maxChangesForBatch = maxChanges ?? 20;
List<string> syncPathsWithChange = syncPaths.Select(x => $"{x}{range}").ToList();
List<ChangesRecord> changes;
if (minChangeNumber == null)
{
changes = await perforce.GetChangesAsync(ChangesOptions.IncludeTimes | ChangesOptions.LongOutput, maxChangesForBatch, ChangeStatus.Submitted, syncPathsWithChange, cancellationToken);
}
else
{
changes = await perforce.GetChangesAsync(ChangesOptions.IncludeTimes | ChangesOptions.LongOutput, clientName: null, minChangeNumber: minChangeNumber.Value, maxChangesForBatch, ChangeStatus.Submitted, userName: null, fileSpecs: syncPathsWithChange, cancellationToken: cancellationToken);
}
// Sort the changes in case we get interleaved output from multiple sync paths
changes = changes.OrderByDescending(x => x.Number).Take(maxChangesForBatch).ToList();
// Add all the previous change numbers to the cache
foreach (ChangesRecord change in changes)
{
if (prevCachedChangeRecord != null)
{
prevCachedChangeRecord.PrevNumber = change.Number;
}
prevCachedChangeRecord = new CachedChangeRecord(change);
string cacheKey = GetChangeCacheKey(change.Number, configHash);
using (ICacheEntry entry = s_changeCache.CreateEntry(cacheKey))
{
entry.SetSize(1);
entry.Value = prevCachedChangeRecord;
}
maxChangeNumber = change.Number - 1;
}
// Return the results
foreach (ChangesRecord change in changes)
{
yield return change;
}
// If we have run out of changes, quit now
if (changes.Count < maxChangesForBatch)
{
break;
}
if (maxChanges != null)
{
maxChanges = maxChanges.Value - changes.Count;
}
}
}
/// <summary>
/// Creates or returns cached <see cref="PerforceChangeDetails"/> objects describing the requested chagnes
/// </summary>
public static IAsyncEnumerable<PerforceChangeDetails> EnumerateChangeDetails(IPerforceConnection perforce, int? minChangeNumber, int? maxChangeNumber, IEnumerable<string> syncPaths, IEnumerable<string> codeRules, CancellationToken cancellationToken)
{
IAsyncEnumerable<int> changeNumbers = EnumerateChanges(perforce, syncPaths, minChangeNumber, maxChangeNumber, null, cancellationToken).Select(x => x.Number);
return EnumerateChangeDetails(perforce, changeNumbers, codeRules, cancellationToken);
}
/// <summary>
/// Creates or returns cached <see cref="PerforceChangeDetails"/> objects describing the requested chagnes
/// </summary>
public static async IAsyncEnumerable<PerforceChangeDetails> EnumerateChangeDetails(IPerforceConnection perforce, IAsyncEnumerable<int> changeNumbers, IEnumerable<string> codeRules, [EnumeratorCancellation] CancellationToken cancellationToken)
{
// Get a delegate that determines if a file is a code change or not
Func<string, bool>? isCodeFile = null;
if (codeRules.Any())
{
FileFilter filter = new FileFilter(PerforceUtils.CodeExtensions.Select(x => $"*{x}"));
foreach (string codeRule in codeRules)
{
filter.AddRule(codeRule);
}
isCodeFile = filter.Matches;
}
// Get the hash of the configuration
Sha1Hash hash = GetConfigHash(perforce, Enumerable.Empty<string>(), codeRules);
// Update them in batches
await using IAsyncEnumerator<int> changeNumberEnumerator = changeNumbers.GetAsyncEnumerator(cancellationToken);
for (; ; )
{
// Get the next batch of changes to query, up to the next change that we already have a cached value for
const int BatchSize = 10;
PerforceChangeDetails? cachedDetails = null;
List<int> changeBatch = new List<int>(BatchSize);
while (changeBatch.Count < BatchSize && await changeNumberEnumerator.MoveNextAsync(cancellationToken))
{
string cacheKey = GetChangeDetailsCacheKey(changeNumberEnumerator.Current, hash);
if (s_changeCache.TryGetValue(cacheKey, out cachedDetails))
{
break;
}
changeBatch.Add(changeNumberEnumerator.Current);
}
// Describe the requested changes
if (changeBatch.Count > 0)
{
const int InitialMaxFiles = 100;
List<DescribeRecord> describeRecords = await perforce.DescribeAsync(DescribeOptions.None, InitialMaxFiles, changeBatch.ToArray(), cancellationToken);
foreach (DescribeRecord describeRecordLoop in describeRecords.OrderByDescending(x => x.Number))
{
DescribeRecord describeRecord = describeRecordLoop;
int queryChangeNumber = describeRecord.Number;
PerforceChangeDetails details = new PerforceChangeDetails(describeRecord, isCodeFile);
// Content only changes must be flagged accurately, because code changes invalidate precompiled binaries. Increase the number of files fetched until we can classify it correctly.
int currentMaxFiles = InitialMaxFiles;
while (describeRecord.Files.Count >= currentMaxFiles && !details.ContainsCode)
{
cancellationToken.ThrowIfCancellationRequested();
currentMaxFiles *= 10;
List<DescribeRecord> newDescribeRecords = await perforce.DescribeAsync(DescribeOptions.None, currentMaxFiles, new int[] { queryChangeNumber }, cancellationToken);
if (newDescribeRecords.Count == 0)
{
break;
}
describeRecord = newDescribeRecords[0];
details = new PerforceChangeDetails(describeRecord, isCodeFile);
}
// Add it to the cache
string cacheKey = GetChangeDetailsCacheKey(describeRecord.Number, hash);
using (ICacheEntry entry = s_changeCache.CreateEntry(cacheKey))
{
entry.SetSize(10);
entry.Value = details;
}
// Return the value
yield return details;
}
}
// Return the cached value, if there was one
if (cachedDetails != null)
{
yield return cachedDetails;
}
else if (changeBatch.Count == 0)
{
yield break;
}
}
}
public static event Action<Exception>? TraceException;
static JsonSerializerOptions GetDefaultJsonSerializerOptions()
{
JsonSerializerOptions options = new JsonSerializerOptions();
options.AllowTrailingCommas = true;
options.ReadCommentHandling = JsonCommentHandling.Skip;
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
public static JsonSerializerOptions DefaultJsonSerializerOptions { get; } = GetDefaultJsonSerializerOptions();
public static bool TryLoadJson<T>(FileReference file, [NotNullWhen(true)] out T? obj) where T : class
{
if (!FileReference.Exists(file))
{
obj = null;
return false;
}
try
{
obj = LoadJson<T>(file);
return true;
}
catch (Exception ex)
{
TraceException?.Invoke(ex);
obj = null;
return false;
}
}
public static T? TryDeserializeJson<T>(byte[] data) where T : class
{
try
{
return JsonSerializer.Deserialize<T>(data, DefaultJsonSerializerOptions)!;
}
catch (Exception ex)
{
TraceException?.Invoke(ex);
return null;
}
}
public static T LoadJson<T>(FileReference file)
{
byte[] data = FileReference.ReadAllBytes(file);
return JsonSerializer.Deserialize<T>(data, DefaultJsonSerializerOptions)!;
}
public static byte[] SerializeJson<T>(T obj)
{
JsonSerializerOptions options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true };
options.Converters.Add(new JsonStringEnumConverter());
byte[] buffer;
using (MemoryStream stream = new MemoryStream())
{
using (Utf8JsonWriter writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
{
JsonSerializer.Serialize(writer, obj, options);
}
buffer = stream.ToArray();
}
return buffer;
}
public static void SaveJson<T>(FileReference file, T obj)
{
byte[] buffer = SerializeJson(obj);
FileReference.WriteAllBytes(file, buffer);
}
public static string GetPathWithCorrectCase(FileInfo info)
{
DirectoryInfo parentInfo = info.Directory!;
if (info.Exists)
{
return Path.Combine(GetPathWithCorrectCase(parentInfo), parentInfo.GetFiles(info.Name)[0].Name);
}
else
{
return Path.Combine(GetPathWithCorrectCase(parentInfo), info.Name);
}
}
public static string GetPathWithCorrectCase(DirectoryInfo info)
{
DirectoryInfo? parentInfo = info.Parent;
if (parentInfo == null)
{
return info.FullName.ToUpperInvariant();
}
else if (info.Exists)
{
return Path.Combine(GetPathWithCorrectCase(parentInfo), parentInfo.GetDirectories(info.Name)[0].Name);
}
else
{
return Path.Combine(GetPathWithCorrectCase(parentInfo), info.Name);
}
}
public static void ForceDeleteFile(string fileName)
{
if (File.Exists(fileName))
{
File.SetAttributes(fileName, File.GetAttributes(fileName) & ~FileAttributes.ReadOnly);
File.Delete(fileName);
}
}
public static bool SpawnProcess(string fileName, string commandLine)
{
using (Process childProcess = new Process())
{
childProcess.StartInfo.FileName = fileName;
childProcess.StartInfo.Arguments = String.IsNullOrEmpty(commandLine) ? "" : commandLine;
childProcess.StartInfo.UseShellExecute = false;
return childProcess.Start();
}
}
public static bool SpawnHiddenProcess(string fileName, string commandLine)
{
using (Process childProcess = new Process())
{
childProcess.StartInfo.FileName = fileName;
childProcess.StartInfo.Arguments = String.IsNullOrEmpty(commandLine) ? "" : commandLine;
childProcess.StartInfo.UseShellExecute = false;
childProcess.StartInfo.RedirectStandardOutput = true;
childProcess.StartInfo.RedirectStandardError = true;
childProcess.StartInfo.CreateNoWindow = true;
try
{
return childProcess.Start();
}
catch
{
return false;
}
}
}
public static async Task<int> ExecuteProcessAsync(string fileName, string? workingDir, string commandLine, Action<string> outputLine, IReadOnlyDictionary<string, string>? env = null, CancellationToken cancellationToken = default)
{
using (ManagedProcessGroup newProcessGroup = new ManagedProcessGroup())
using (ManagedProcess newProcess = new ManagedProcess(newProcessGroup, fileName, commandLine, workingDir, env, null, ProcessPriorityClass.Normal))
{
for (; ; )
{
string? line = await newProcess.ReadLineAsync(cancellationToken);
if (line == null)
{
await newProcess.WaitForExitAsync(cancellationToken);
return newProcess.ExitCode;
}
outputLine(line);
}
}
}
public static async Task<int> ExecuteProcessAsync(string fileName, string? workingDir, string commandLine, Action<string> outputLine, CancellationToken cancellationToken)
{
using (ManagedProcessGroup newProcessGroup = new ManagedProcessGroup())
using (ManagedProcess newProcess = new ManagedProcess(newProcessGroup, fileName, commandLine, workingDir, null, null, ProcessPriorityClass.Normal))
{
for (; ; )
{
string? line = await newProcess.ReadLineAsync(cancellationToken);
if (line == null)
{
await newProcess.WaitForExitAsync(cancellationToken);
return newProcess.ExitCode;
}
outputLine(line);
}
}
}
public static bool SafeIsFileUnderDirectory(string fileName, string directoryName)
{
try
{
string fullDirectoryName = Path.GetFullPath(directoryName).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
string fullFileName = Path.GetFullPath(fileName);
return fullFileName.StartsWith(fullDirectoryName, StringComparison.InvariantCultureIgnoreCase);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Expands variables in $(VarName) format in the given string. Variables are retrieved from the given dictionary, or through the environment of the current process.
/// Any unknown variables are ignored.
/// </summary>
/// <param name="inputString">String to search for variable names</param>
/// <param name="additionalVariables">Lookup of variable names to values</param>
/// <returns>String with all variables replaced</returns>
public static string ExpandVariables(string inputString, Dictionary<string, string>? additionalVariables = null)
{
string result = inputString;
for (int idx = result.IndexOf("$(", StringComparison.Ordinal); idx != -1; idx = result.IndexOf("$(", idx, StringComparison.Ordinal))
{
// Find the end of the variable name
int endIdx = result.IndexOf(")", idx + 2, StringComparison.Ordinal);
if (endIdx == -1)
{
break;
}
// Extract the variable name from the string
string name = result.Substring(idx + 2, endIdx - (idx + 2));
// Strip the format from the name
string? format = null;
int formatIdx = name.IndexOf(':', StringComparison.Ordinal);
if (formatIdx != -1)
{
format = name.Substring(formatIdx + 1);
name = name.Substring(0, formatIdx);
}
// Find the value for it, either from the dictionary or the environment block
string? value;
if (additionalVariables == null || !additionalVariables.TryGetValue(name, out value))
{
value = Environment.GetEnvironmentVariable(name);
if (value == null)
{
idx = endIdx + 1;
continue;
}
}
// Encode the variable if necessary
if (format != null)
{
if (String.Equals(format, "URI", StringComparison.OrdinalIgnoreCase))
{
value = Uri.EscapeDataString(value);
}
}
// Replace the variable, or skip past it
result = result.Substring(0, idx) + value + result.Substring(endIdx + 1);
}
return result;
}
class ProjectJson
{
public bool Enterprise { get; set; }
}
/// <summary>
/// Determines if a project is an enterprise project
/// </summary>
public static bool IsEnterpriseProjectFromText(string text)
{
try
{
JsonSerializerOptions options = new JsonSerializerOptions();
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new JsonStringEnumConverter());
ProjectJson project = JsonSerializer.Deserialize<ProjectJson>(text, options)!;
return project.Enterprise;
}
catch
{
return false;
}
}
/******/
private static void AddLocalConfigPaths_WithSubFolders(DirectoryInfo baseDir, string fileName, List<FileInfo> files)
{
if (baseDir.Exists)
{
FileInfo baseFileInfo = new FileInfo(Path.Combine(baseDir.FullName, fileName));
if (baseFileInfo.Exists)
{
files.Add(baseFileInfo);
}
foreach (DirectoryInfo subDirInfo in baseDir.EnumerateDirectories())
{
FileInfo subFile = new FileInfo(Path.Combine(subDirInfo.FullName, fileName));
if (subFile.Exists)
{
files.Add(subFile);
}
}
}
}
private static void AddLocalConfigPaths_WithExtensionDirs(DirectoryInfo? baseDir, string relativePath, string fileName, List<FileInfo> files)
{
if (baseDir != null && baseDir.Exists)
{
AddLocalConfigPaths_WithSubFolders(new DirectoryInfo(Path.Combine(baseDir.FullName, relativePath)), fileName, files);
DirectoryInfo platformExtensionsDir = new DirectoryInfo(Path.Combine(baseDir.FullName, "Platforms"));
if (platformExtensionsDir.Exists)
{
foreach (DirectoryInfo platformExtensionDir in platformExtensionsDir.EnumerateDirectories())
{
AddLocalConfigPaths_WithSubFolders(new DirectoryInfo(Path.Combine(platformExtensionDir.FullName, relativePath)), fileName, files);
}
}
DirectoryInfo restrictedBaseDir = new DirectoryInfo(Path.Combine(baseDir.FullName, "Restricted"));
if (restrictedBaseDir.Exists)
{
foreach (DirectoryInfo restrictedDir in restrictedBaseDir.EnumerateDirectories())
{
AddLocalConfigPaths_WithSubFolders(new DirectoryInfo(Path.Combine(restrictedDir.FullName, relativePath)), fileName, files);
}
}
}
}
public static List<FileInfo> GetLocalConfigPaths(DirectoryInfo engineDir, FileInfo projectFile)
{
List<FileInfo> searchPaths = new List<FileInfo>();
AddLocalConfigPaths_WithExtensionDirs(engineDir, "Programs/UnrealGameSync", "UnrealGameSync.ini", searchPaths);
if (projectFile.Name.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase))
{
AddLocalConfigPaths_WithExtensionDirs(projectFile.Directory!, "Build", "UnrealGameSync.ini", searchPaths);
}
else if (projectFile.Name.EndsWith(".uefnproject", StringComparison.OrdinalIgnoreCase))
{
AddLocalConfigPaths_WithExtensionDirs(projectFile.Directory?.Parent, "Build", "UnrealGameSync.ini", searchPaths);
}
else
{
AddLocalConfigPaths_WithExtensionDirs(engineDir, "Programs/UnrealGameSync", "DefaultEngine.ini", searchPaths);
}
return searchPaths;
}
/******/
private static void AddDepotConfigPaths_PlatformFolders(string basePath, string fileName, List<string> searchPaths)
{
searchPaths.Add(String.Format("{0}/{1}", basePath, fileName));
searchPaths.Add(String.Format("{0}/*/{1}", basePath, fileName));
}
private static void AddDepotConfigPaths_PlatformExtensions(string basePath, string relativePath, string fileName, List<string> searchPaths)
{
AddDepotConfigPaths_PlatformFolders(basePath + relativePath, fileName, searchPaths);
AddDepotConfigPaths_PlatformFolders(basePath + "/Platforms/*" + relativePath, fileName, searchPaths);
AddDepotConfigPaths_PlatformFolders(basePath + "/Restricted/*" + relativePath, fileName, searchPaths);
}
public static List<string> GetDepotConfigPaths(string enginePath, string projectPath)
{
List<string> searchPaths = new List<string>();
AddDepotConfigPaths_PlatformExtensions(enginePath, "/Programs/UnrealGameSync", "UnrealGameSync.ini", searchPaths);
if (projectPath.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase))
{
AddDepotConfigPaths_PlatformExtensions(projectPath.Substring(0, projectPath.LastIndexOf('/')), "/Build", "UnrealGameSync.ini", searchPaths);
}
else if (projectPath.EndsWith(".uefnproject", StringComparison.OrdinalIgnoreCase))
{
string parentFolder = projectPath.Substring(0, projectPath.LastIndexOf('/'));
parentFolder = parentFolder.Substring(0, parentFolder.LastIndexOf('/'));
AddDepotConfigPaths_PlatformExtensions(parentFolder, "/Build", "UnrealGameSync.ini", searchPaths);
}
else
{
AddDepotConfigPaths_PlatformExtensions(enginePath, "/Programs/UnrealGameSync", "DefaultEngine.ini", searchPaths);
}
return searchPaths;
}
/******/
public static async Task<string[]?> TryPrintFileUsingCacheAsync(IPerforceConnection perforce, string depotPath, DirectoryReference cacheFolder, string? digest, ILogger logger, CancellationToken cancellationToken)
{
if (digest == null)
{
PerforceResponse<PrintRecord<string[]>> printLinesResponse = await perforce.TryPrintLinesAsync(depotPath, cancellationToken);
if (printLinesResponse.Succeeded)
{
return printLinesResponse.Data.Contents;
}
else
{
return null;
}
}
FileReference cacheFile = FileReference.Combine(cacheFolder, digest);
if (FileReference.Exists(cacheFile))
{
logger.LogDebug("Reading cached copy of {DepotFile} from {LocalFile}", depotPath, cacheFile);
try
{
string[] lines = await FileReference.ReadAllLinesAsync(cacheFile, cancellationToken);
try
{
FileReference.SetLastWriteTimeUtc(cacheFile, DateTime.UtcNow);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Exception touching cache file {LocalFile}", cacheFile);
}
return lines;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Error while reading cache file {LocalFile}: {Message}", cacheFile, ex.Message);
}
}
DirectoryReference.CreateDirectory(cacheFolder);
FileReference tempFile = new FileReference(String.Format("{0}.{1}.temp", cacheFile.FullName, Guid.NewGuid()));
PerforceResponseList<PrintRecord> printResponse = await perforce.TryPrintAsync(tempFile.FullName, depotPath, cancellationToken);
if (!printResponse.Succeeded)
{
return null;
}
else
{
string[] lines = await FileReference.ReadAllLinesAsync(tempFile, cancellationToken);
try
{
FileReference.SetAttributes(tempFile, FileAttributes.Normal);
FileReference.SetLastWriteTimeUtc(tempFile, DateTime.UtcNow);
FileReference.Move(tempFile, cacheFile);
}
catch
{
try
{
FileReference.Delete(tempFile);
}
catch
{
}
}
return lines;
}
}
public static void ClearPrintCache(DirectoryReference cacheFolder)
{
DirectoryInfo cacheDir = cacheFolder.ToDirectoryInfo();
if (cacheDir.Exists)
{
DateTime deleteTime = DateTime.UtcNow - TimeSpan.FromDays(5.0);
foreach (FileInfo cacheFile in cacheDir.EnumerateFiles())
{
if (cacheFile.LastWriteTimeUtc < deleteTime || cacheFile.Name.EndsWith(".temp", StringComparison.OrdinalIgnoreCase))
{
try
{
cacheFile.Attributes = FileAttributes.Normal;
cacheFile.Delete();
}
catch
{
}
}
}
}
}
public static Color Blend(Color first, Color second, float t)
{
return Color.FromArgb((int)(first.R + (second.R - first.R) * t), (int)(first.G + (second.G - first.G) * t), (int)(first.B + (second.B - first.B) * t));
}
public static PerforceSettings OverridePerforceSettings(IPerforceSettings defaultConnection, string? serverAndPort, string? userName)
{
PerforceSettings newSettings = new PerforceSettings(defaultConnection);
if (!String.IsNullOrWhiteSpace(serverAndPort))
{
newSettings.ServerAndPort = serverAndPort;
}
if (!String.IsNullOrWhiteSpace(userName))
{
newSettings.UserName = userName;
}
return newSettings;
}
public static string FormatRecentDateTime(DateTime date)
{
DateTime now = DateTime.Now;
DateTime midnight = new DateTime(now.Year, now.Month, now.Day);
DateTime midnightTonight = midnight + TimeSpan.FromDays(1.0);
if (date > midnightTonight)
{
return String.Format("{0} at {1}", date.ToLongDateString(), date.ToShortTimeString());
}
else if (date >= midnight)
{
return String.Format("today at {0}", date.ToShortTimeString());
}
else if (date >= midnight - TimeSpan.FromDays(1.0))
{
return String.Format("yesterday at {0}", date.ToShortTimeString());
}
else if (date >= midnight - TimeSpan.FromDays(5.0))
{
return String.Format("{0:dddd} at {1}", date, date.ToShortTimeString());
}
else
{
return String.Format("{0} at {1}", date.ToLongDateString(), date.ToShortTimeString());
}
}
public static string FormatDurationMinutes(TimeSpan duration)
{
return FormatDurationMinutes((int)(duration.TotalMinutes + 1));
}
public static string FormatDurationMinutes(int totalMinutes)
{
if (totalMinutes > 24 * 60)
{
return String.Format("{0}d {1}h", totalMinutes / (24 * 60), (totalMinutes / 60) % 24);
}
else if (totalMinutes > 60)
{
return String.Format("{0}h {1}m", totalMinutes / 60, totalMinutes % 60);
}
else
{
return String.Format("{0}m", totalMinutes);
}
}
public static string FormatUserName(string userName)
{
StringBuilder normalUserName = new StringBuilder();
for (int idx = 0; idx < userName.Length; idx++)
{
if (idx == 0 || userName[idx - 1] == '.')
{
normalUserName.Append(Char.ToUpper(userName[idx]));
}
else if (userName[idx] == '.')
{
normalUserName.Append(' ');
}
else
{
normalUserName.Append(Char.ToLower(userName[idx]));
}
}
return normalUserName.ToString();
}
public static void OpenUrl(string url)
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = url;
startInfo.UseShellExecute = true;
using Process? _ = Process.Start(startInfo);
}
[SupportedOSPlatform("windows")]
public static void DeleteRegistryKey(RegistryKey rootKey, string keyName, string valueName)
{
using (RegistryKey? key = rootKey.OpenSubKey(keyName, true))
{
if (key != null)
{
DeleteRegistryKey(key, valueName);
}
}
}
[SupportedOSPlatform("windows")]
public static void DeleteRegistryKey(RegistryKey key, string name)
{
string[] valueNames = key.GetValueNames();
if (valueNames.Any(x => String.Equals(x, name, StringComparison.OrdinalIgnoreCase)))
{
try
{
key.DeleteValue(name);
}
catch
{
}
}
}
}
}