Files
UnrealEngine/Engine/Source/Programs/UnrealGameSync/UnrealGameSyncShared/OpenProjectInfo.cs
2025-05-18 13:04:45 +08:00

354 lines
16 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Perforce;
using Microsoft.Extensions.Logging;
namespace UnrealGameSync
{
public class OpenProjectInfo
{
public UserSelectedProjectSettings SelectedProject { get; }
public IPerforceSettings PerforceSettings { get; }
public ProjectInfo ProjectInfo { get; }
public UserWorkspaceSettings WorkspaceSettings { get; }
public WorkspaceStateWrapper WorkspaceStateWrapper { get; }
public ConfigFile LatestProjectConfigFile { get; }
public ConfigFile WorkspaceProjectConfigFile { get; }
public IReadOnlyList<string>? WorkspaceProjectStreamFilter { get; }
public List<KeyValuePair<FileReference, DateTime>> LocalConfigFiles { get; }
public bool GenerateP4Config { get; }
public OpenProjectInfo(UserSelectedProjectSettings selectedProject, IPerforceSettings perforceSettings, ProjectInfo projectInfo, UserWorkspaceSettings workspaceSettings, WorkspaceStateWrapper workspaceStateWrapper, ConfigFile latestProjectConfigFile, ConfigFile workspaceProjectConfigFile, IReadOnlyList<string>? workspaceProjectStreamFilter, List<KeyValuePair<FileReference, DateTime>> localConfigFiles, bool generateP4Config)
{
SelectedProject = selectedProject;
PerforceSettings = perforceSettings;
ProjectInfo = projectInfo;
WorkspaceSettings = workspaceSettings;
WorkspaceStateWrapper = workspaceStateWrapper;
LatestProjectConfigFile = latestProjectConfigFile;
WorkspaceProjectConfigFile = workspaceProjectConfigFile;
WorkspaceProjectStreamFilter = workspaceProjectStreamFilter;
LocalConfigFiles = localConfigFiles;
GenerateP4Config = generateP4Config;
}
public static async Task<OpenProjectInfo> CreateAsync(IPerforceSettings defaultPerforceSettings, UserSelectedProjectSettings selectedProject, UserSettings userSettings, bool generateP4Config, ILogger<OpenProjectInfo> logger, CancellationToken cancellationToken)
{
PerforceSettings perforceSettings = Utility.OverridePerforceSettings(defaultPerforceSettings, selectedProject.ServerAndPort, selectedProject.UserName);
using IPerforceConnection perforce = await PerforceConnection.CreateAsync(perforceSettings, logger);
// Make sure we're logged in
PerforceResponse<LoginRecord> loginState = await perforce.TryGetLoginStateAsync(cancellationToken);
if (!loginState.Succeeded)
{
throw new UserErrorException("User is not logged in to Perforce.");
}
// Execute like a regular task
return await CreateAsync(perforce, selectedProject, userSettings, generateP4Config, logger, cancellationToken);
}
public static async Task<OpenProjectInfo> CreateAsync(IPerforceConnection defaultConnection, UserSelectedProjectSettings selectedProject, UserSettings userSettings, bool generateP4Config, ILogger<OpenProjectInfo> logger, CancellationToken cancellationToken)
{
using IDisposable? loggerScope = logger.BeginScope("Project {SelectedProject}", selectedProject.ToString());
logger.LogInformation("Detecting settings for {Project}", selectedProject);
// Use the cached client path to the file if it's available; it's much quicker than trying to find the correct workspace.
IPerforceConnection? perforceClient = null;
try
{
IPerforceSettings perforceSettings;
FileReference newSelectedFileName;
string newSelectedClientFileName;
if (!String.IsNullOrEmpty(selectedProject.ClientPath))
{
// Get the client path
newSelectedClientFileName = selectedProject.ClientPath;
// Get the client name
string? clientName;
if (!PerforceUtils.TryGetClientName(newSelectedClientFileName, out clientName))
{
throw new UserErrorException($"Couldn't get client name from {newSelectedClientFileName}");
}
// Create the client
perforceSettings = new PerforceSettings(defaultConnection.Settings) { ClientName = clientName };
perforceClient = await PerforceConnection.CreateAsync(perforceSettings, logger);
// Figure out the path on the client. Use the cached location if it's valid.
string? localPath = selectedProject.LocalPath;
if (localPath == null || !File.Exists(localPath))
{
List<WhereRecord> records = await perforceClient.WhereAsync(newSelectedClientFileName, cancellationToken).Where(x => !x.Unmap).ToListAsync(cancellationToken);
if (records.Count != 1)
{
throw new UserErrorException($"Couldn't get client path for {newSelectedClientFileName}");
}
localPath = Path.GetFullPath(records[0].Path);
}
newSelectedFileName = new FileReference(localPath);
}
else
{
// Get the perforce server settings
InfoRecord perforceInfo = await defaultConnection.GetInfoAsync(InfoOptions.ShortOutput, cancellationToken);
// Use the path as the selected filename
newSelectedFileName = new FileReference(selectedProject.LocalPath!);
// Make sure the project exists
if (!FileReference.Exists(newSelectedFileName))
{
throw new UserErrorException($"{selectedProject.LocalPath} does not exist.");
}
// Find all the clients for this user
logger.LogInformation("Enumerating clients for {UserName}...", perforceInfo.UserName);
List<ClientsRecord> clients = await defaultConnection.GetClientsAsync(ClientsOptions.None, defaultConnection.Settings.UserName, cancellationToken);
List<IPerforceSettings> candidateClients = await FilterClients(clients, newSelectedFileName, defaultConnection.Settings, perforceInfo.ClientHost, logger, cancellationToken);
if (candidateClients.Count == 0)
{
// Search through all workspaces. We may find a suitable workspace which is for any user.
logger.LogInformation("Enumerating shared clients...");
clients = await defaultConnection.GetClientsAsync(ClientsOptions.None, "", cancellationToken);
// Filter this list of clients
candidateClients = await FilterClients(clients, newSelectedFileName, defaultConnection.Settings, perforceInfo.ClientHost, logger, cancellationToken);
#pragma warning disable CA1508 // False positive? "warning CA1508: 'newCandidateClients.Count == 0' is always 'true'. Remove or refactor the condition(s) to avoid dead code."
// If we still couldn't find any, fail.
if (candidateClients.Count == 0)
{
throw new UserErrorException($"Couldn't find any Perforce workspace containing {newSelectedFileName}. Check your connection settings.");
}
#pragma warning restore CA1508
}
// Check there's only one client
if (candidateClients.Count > 1)
{
throw new UserErrorException(String.Format("Found multiple workspaces containing {0}:\n\n{1}\n\nCannot determine which to use.", Path.GetFileName(newSelectedFileName.GetFileName()), String.Join("\n", candidateClients.Select(x => x.ClientName))));
}
// Take the client we've chosen
perforceSettings = candidateClients[0];
perforceClient = await PerforceConnection.CreateAsync(perforceSettings, logger);
// Get the client path for the project file
List<WhereRecord> records = await perforceClient.WhereAsync(newSelectedFileName.FullName, cancellationToken).Where(x => !x.Unmap).ToListAsync(cancellationToken);
if (records.Count == 0)
{
throw new UserErrorException("File is not mapped to any client");
}
else if (records.Count > 1)
{
throw new UserErrorException($"File is mapped to {records.Count} locations: {String.Join(", ", records.Select(x => x.Path))}");
}
newSelectedClientFileName = records[0].ClientFile;
}
// Make sure the drive containing the project exists, to prevent other errors down the line
string pathRoot = Path.GetPathRoot(newSelectedFileName.FullName)!;
if (!Directory.Exists(pathRoot))
{
throw new UserErrorException($"Path '{newSelectedFileName}' is invalid");
}
// Make sure the path case is correct. This can cause UBT intermediates to be out of date if the case mismatches.
newSelectedFileName = FileReference.FindCorrectCase(newSelectedFileName);
// Update the selected project with all the data we've found
selectedProject = new UserSelectedProjectSettings(selectedProject.ServerAndPort, selectedProject.UserName, selectedProject.Type, newSelectedClientFileName, newSelectedFileName.FullName);
// Get the local branch root
string? branchClientPath = null;
DirectoryReference? branchDirectoryName = null;
// Figure out where the engine is in relation to it
int endIdx = newSelectedClientFileName.Length - 1;
if (endIdx != -1 && newSelectedClientFileName.EndsWith(".uproject", StringComparison.InvariantCultureIgnoreCase))
{
endIdx = newSelectedClientFileName.LastIndexOf('/') - 1;
}
for (; endIdx >= 2; endIdx--)
{
if (newSelectedClientFileName[endIdx] == '/')
{
List<PerforceResponse<FStatRecord>> fileRecords = await perforceClient.TryFStatAsync(FStatOptions.None, newSelectedClientFileName.Substring(0, endIdx) + "/Engine/Build/Build.version", cancellationToken).ToListAsync(cancellationToken);
if (fileRecords.Succeeded() && fileRecords.Count > 0)
{
FStatRecord fileRecord = fileRecords[0].Data;
if (fileRecord.ClientFile == null)
{
throw new UserErrorException($"Missing client path for {fileRecord.DepotFile}");
}
branchClientPath = newSelectedClientFileName.Substring(0, endIdx);
branchDirectoryName = new FileReference(fileRecord.ClientFile).Directory.ParentDirectory?.ParentDirectory;
break;
}
}
}
if (branchClientPath == null || branchDirectoryName == null)
{
throw new UserErrorException($"Could not find engine in Perforce relative to project path ({newSelectedClientFileName})");
}
logger.LogInformation("Found branch root at {RootPath}", branchClientPath);
// Read the existing workspace settings from disk, and update them with any info computed here
int branchIdx = branchClientPath.IndexOf('/', 2);
string branchPath = (branchIdx == -1) ? String.Empty : branchClientPath.Substring(branchIdx);
string projectPath = newSelectedClientFileName.Substring(branchClientPath.Length);
UserWorkspaceSettings userWorkspaceSettings = userSettings.FindOrAddWorkspaceSettings(branchDirectoryName, perforceSettings.ServerAndPort, perforceSettings.UserName, perforceSettings.ClientName!, branchPath, projectPath, logger);
// Now compute the updated project info
ProjectInfo projectInfo = await ProjectInfo.CreateAsync(perforceClient, userWorkspaceSettings, cancellationToken);
// Update the cached workspace state
WorkspaceStateWrapper? workspaceStateWrapper = null;
try
{
#pragma warning disable CA2000
workspaceStateWrapper = userSettings.FindOrAddWorkspaceState(projectInfo, userWorkspaceSettings);
#pragma warning restore CA2000
// Read the initial config file
List<KeyValuePair<FileReference, DateTime>> localConfigFiles = new List<KeyValuePair<FileReference, DateTime>>();
ConfigFile latestProjectConfigFile = await ConfigUtils.ReadProjectConfigFileAsync(perforceClient, projectInfo, localConfigFiles, logger, cancellationToken);
// Get the local config file and stream filter
ConfigFile workspaceProjectConfigFile = await WorkspaceUpdate.ReadProjectConfigFile(branchDirectoryName, newSelectedFileName, logger);
IReadOnlyList<string>? workspaceProjectStreamFilter = await WorkspaceUpdate.ReadProjectStreamFilter(perforceClient, workspaceProjectConfigFile, cancellationToken);
OpenProjectInfo workspaceSettings = new OpenProjectInfo(selectedProject, perforceSettings, projectInfo, userWorkspaceSettings, workspaceStateWrapper, latestProjectConfigFile, workspaceProjectConfigFile, workspaceProjectStreamFilter, localConfigFiles, generateP4Config);
if (generateP4Config)
{
GenerateP4ConfigFile(perforceSettings, projectInfo, logger);
}
return workspaceSettings;
}
catch
{
workspaceStateWrapper?.Dispose();
throw;
}
}
finally
{
perforceClient?.Dispose();
}
}
static async Task<List<IPerforceSettings>> FilterClients(List<ClientsRecord> clients, FileReference newSelectedFileName, IPerforceSettings defaultPerforceSettings, string? hostName, ILogger logger, CancellationToken cancellationToken)
{
List<IPerforceSettings> candidateClients = new List<IPerforceSettings>();
foreach (ClientsRecord client in clients)
{
// Make sure the client is well formed
if (!String.IsNullOrEmpty(client.Name) && (!String.IsNullOrEmpty(client.Host) || !String.IsNullOrEmpty(client.Owner)) && !String.IsNullOrEmpty(client.Root))
{
// Require either a username or host name match
if ((String.IsNullOrEmpty(client.Host) || String.Equals(client.Host, hostName, StringComparison.OrdinalIgnoreCase)) && (String.IsNullOrEmpty(client.Owner) || String.Equals(client.Owner, defaultPerforceSettings.UserName, StringComparison.OrdinalIgnoreCase)))
{
if (!Utility.SafeIsFileUnderDirectory(newSelectedFileName.FullName, client.Root))
{
logger.LogInformation("Rejecting {ClientName} due to root mismatch ({RootPath})", client.Name, client.Root);
continue;
}
PerforceSettings candidateSettings = new PerforceSettings(defaultPerforceSettings) { ClientName = client.Name };
using IPerforceConnection candidateClient = await PerforceConnection.CreateAsync(candidateSettings, logger);
List<PerforceResponse<WhereRecord>> whereRecords = await candidateClient.TryWhereAsync(newSelectedFileName.FullName, cancellationToken).Where(x => x.Failed || !x.Data.Unmap).ToListAsync(cancellationToken);
if (!whereRecords.Succeeded() || whereRecords.Count != 1)
{
logger.LogInformation("Rejecting {ClientName} due to file not existing in workspace", client.Name);
continue;
}
List<PerforceResponse<FStatRecord>> records = await candidateClient.TryFStatAsync(FStatOptions.None, newSelectedFileName.FullName, cancellationToken).ToListAsync(cancellationToken);
if (!records.Succeeded())
{
logger.LogInformation("Rejecting {ClientName} due to {FileName} not in depot", client.Name, newSelectedFileName);
continue;
}
records.RemoveAll(x => !x.Data.IsMapped);
if (records.Count == 0)
{
logger.LogInformation("Rejecting {ClientName} due to {NumRecords} matching records", client.Name, records.Count);
continue;
}
logger.LogInformation("Found valid client {ClientName}", client.Name);
candidateClients.Add(candidateSettings);
}
}
}
return candidateClients;
}
static void GenerateP4ConfigFile(IPerforceSettings perforceSettings, ProjectInfo projectInfo, ILogger<OpenProjectInfo> logger)
{
string? p4ConfigName = PerforceEnvironment.Default.GetValue("P4CONFIG");
// We only create a p4config if the user has opted into the system by setting P4CONFIG manually.
if (String.IsNullOrEmpty(p4ConfigName))
{
logger.LogError("Could not generate a p4config file as the envvar P4CONFIG is not set!");
return;
}
string configFilePath = Path.Combine(projectInfo.LocalRootPath.ToString(), p4ConfigName);
// Do not update or replace the file if it already exists
if (File.Exists(configFilePath))
{
logger.LogDebug("P4CONFIG file '{Path}' already exists.", configFilePath);
return;
}
Dictionary<string, string> settings = new Dictionary<string, string>();
settings.Add("P4PORT", perforceSettings.ServerAndPort);
settings.Add("P4USER", perforceSettings.UserName);
settings.Add("P4CLIENT", perforceSettings.ClientName ?? "");
List<string> Lines = new List<string>();
foreach (KeyValuePair<string, string> setting in settings)
{
Lines.Add(String.Format("{0}={1}", setting.Key, setting.Value));
}
try
{
File.WriteAllLines(configFilePath, Lines);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to write {Path} with exception - {Msg}", p4ConfigName, ex.Message);
return;
}
logger.LogInformation("Successfully wrote a P4CONFIG file to '{Path}'.", configFilePath);
}
}
}