// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnrealBuildTool; using EpicGames.Core; using EpicGames.Perforce; using System.Threading.Tasks; using System.Diagnostics; using Microsoft.Extensions.Logging; using static AutomationTool.CommandUtils; using UnrealBuildBase; namespace AutomationTool { /// /// Contains detected Perforce settings. /// public class P4Environment { /// /// The Perforce host and port number (eg. perforce:1666) /// public string ServerAndPort { get; private set; } /// /// The Perforce username /// public string User { get; private set; } /// /// Name of the Perforce client containing UAT. /// public string Client { get; private set; } /// /// The current branch containing UAT. This may be a depot path (e.g. //depot/UE4) or the name of a stream (e.g. //UE5/Main). /// public string Branch { get; private set; } /// /// Path to the root of the branch as a client path (//MyClient/UE). /// public string ClientRoot { get; private set; } /// /// The currently synced changelist. /// public int Changelist { get { if (ChangelistInternal <= 0) { throw new AutomationException("P4Environment.Changelist has not been initialized but is requested. Set uebp_CL env var or run UAT with -P4 to automatically detect changelist."); } return ChangelistInternal; } protected set { ChangelistInternal = value; } } /// /// Backing value for the current changelist property /// private int ChangelistInternal = -1; /// /// The currently synced code changelist. /// public int CodeChangelist { get { if (CodeChangelistInternal <= 0) { throw new AutomationException("P4Environment.CodeChangelist has not been initialized but is requested. Set uebp_CodeCL env var or run UAT with -P4 to automatically detect changelist."); } return CodeChangelistInternal; } protected set { CodeChangelistInternal = value; } } /// /// Backing value for the current code changelist /// private int CodeChangelistInternal = -1; static ILogger Logger => Log.Logger; /// /// Constructor. Derives the Perforce environment settings. /// internal P4Environment(CommandEnvironment CmdEnv) { // Get the Perforce port setting ServerAndPort = CommandUtils.ParseParamValue(Environment.GetCommandLineArgs(), "-p4port", null); if(String.IsNullOrEmpty(ServerAndPort)) { // check env var ServerAndPort = CommandUtils.GetEnvVar(EnvVarNames.P4Port); if (String.IsNullOrEmpty(ServerAndPort)) { ServerAndPort = DetectP4Port(); } CommandUtils.SetEnvVar(EnvVarNames.P4Port, ServerAndPort); } // Get the Perforce user setting User = CommandUtils.ParseParamValue(Environment.GetCommandLineArgs(), "-p4user", null); if (String.IsNullOrEmpty(User)) { User = CommandUtils.GetEnvVar(EnvVarNames.User); if (String.IsNullOrEmpty(User)) { P4Connection DefaultConnection = new P4Connection(User: null, Client: null, ServerAndPort: ServerAndPort); User = DetectUserName(DefaultConnection); } CommandUtils.SetEnvVar(EnvVarNames.User, User); } // Get the Perforce client setting Client = CommandUtils.GetEnvVar(EnvVarNames.Client); Branch = CommandUtils.GetEnvVar(EnvVarNames.BuildRootP4); ClientRoot = CommandUtils.GetEnvVar(EnvVarNames.ClientRoot); if(String.IsNullOrEmpty(Client) || String.IsNullOrEmpty(Branch) || String.IsNullOrEmpty(ClientRoot)) { // Create a connection using the current setting P4Connection DefaultConnection = new P4Connection(User: User, Client: null, ServerAndPort: ServerAndPort); // Get the client info P4ClientInfo ThisClient; if(String.IsNullOrEmpty(Client)) { string HostName = Unreal.MachineName; ThisClient = DetectClient(DefaultConnection, User, HostName, CmdEnv.AutomationToolDll); Logger.LogInformation("Using user {User} clientspec {ClientName} {ClientPath}", User, ThisClient.Name, ThisClient.RootPath); Client = ThisClient.Name; CommandUtils.SetEnvVar(EnvVarNames.Client, Client); } else { ThisClient = DefaultConnection.GetClientInfo(Client); } // Detect the root paths if(String.IsNullOrEmpty(Branch) || String.IsNullOrEmpty(ClientRoot)) { P4Connection ClientConnection = new P4Connection(User: User, Client: ThisClient.Name, ServerAndPort: ServerAndPort); string BranchPath; string ClientRootPath; DetectRootPaths(ClientConnection, CmdEnv.LocalRoot, ThisClient, out BranchPath, out ClientRootPath); if(String.IsNullOrEmpty(Branch)) { Branch = BranchPath; CommandUtils.SetEnvVar(EnvVarNames.BuildRootP4, Branch); } if(String.IsNullOrEmpty(ClientRoot)) { ClientRoot = ClientRootPath; CommandUtils.SetEnvVar(EnvVarNames.ClientRoot, ClientRootPath); } } } // We expect the build root to not end with a path separator if (Branch.EndsWith("/")) { Branch = Branch.TrimEnd('/'); CommandUtils.SetEnvVar(EnvVarNames.BuildRootP4, Branch); } // Set the current changelist string ChangelistString = CommandUtils.GetEnvVar(EnvVarNames.Changelist, null); if(String.IsNullOrEmpty(ChangelistString) && CommandUtils.P4CLRequired) { P4Connection Connection = new P4Connection(User, Client, ServerAndPort); ChangelistString = DetectCurrentCL(Connection, ClientRoot); CommandUtils.SetEnvVar(EnvVarNames.Changelist, ChangelistString); } if(!String.IsNullOrEmpty(ChangelistString)) { Changelist = int.Parse(ChangelistString); } // Set the current code changelist string CodeChangelistString = CommandUtils.GetEnvVar(EnvVarNames.CodeChangelist); if(String.IsNullOrEmpty(CodeChangelistString) && CommandUtils.P4CLRequired) { Stopwatch Timer = Stopwatch.StartNew(); using PerforceConnection Connection = new PerforceConnection(new PerforceSettings(ServerAndPort, User) { ClientName = Client }, Log.Logger); CodeChangelistString = DetectCurrentCodeCL(Connection, Changelist).Result.ToString(); CommandUtils.SetEnvVar(EnvVarNames.CodeChangelist, CodeChangelistString); Logger.LogDebug("Took {ElapsedMs}ms to query last code change", Timer.ElapsedMilliseconds); } if(!String.IsNullOrEmpty(CodeChangelistString)) { CodeChangelist = int.Parse(CodeChangelistString); } // Set the standard environment variables based on the values we've found CommandUtils.SetEnvVar("P4PORT", ServerAndPort); CommandUtils.SetEnvVar("P4USER", User); CommandUtils.SetEnvVar("P4CLIENT", Client); // Write a summary of the settings to the output window if (!CommandUtils.CmdEnv.IsChildInstance) { Logger.LogInformation("Detected Perforce Settings:"); Logger.LogInformation(" Server: {ServerAndPort}", ServerAndPort); Logger.LogInformation(" User: {User}", User); Logger.LogInformation(" Client: {Client}", Client); Logger.LogInformation(" Branch: {Branch}", Branch); if (ChangelistInternal != -1) { Logger.LogInformation(" Last Change: {Changelist}", Changelist); } if (CodeChangelistInternal != -1) { Logger.LogInformation(" Last Code Change: {CodeChangelist}", CodeChangelist); } } // Write all the environment variables to the log Logger.LogDebug("Perforce Environment Variables:"); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.P4Port, InternalUtils.GetEnvironmentVariable(EnvVarNames.P4Port, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.User, InternalUtils.GetEnvironmentVariable(EnvVarNames.User, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.Client, InternalUtils.GetEnvironmentVariable(EnvVarNames.Client, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.BuildRootP4, InternalUtils.GetEnvironmentVariable(EnvVarNames.BuildRootP4, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.BuildRootEscaped, InternalUtils.GetEnvironmentVariable(EnvVarNames.BuildRootEscaped, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.ClientRoot, InternalUtils.GetEnvironmentVariable(EnvVarNames.ClientRoot, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.Changelist, InternalUtils.GetEnvironmentVariable(EnvVarNames.Changelist, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", EnvVarNames.CodeChangelist, InternalUtils.GetEnvironmentVariable(EnvVarNames.CodeChangelist, "", true)); Logger.LogDebug(" {Arg0}={Arg1}", "P4PORT", InternalUtils.GetEnvironmentVariable("P4PORT", "", true)); Logger.LogDebug(" {Arg0}={Arg1}", "P4USER", InternalUtils.GetEnvironmentVariable("P4USER", "", true)); Logger.LogDebug(" {Arg0}={Arg1}", "P4CLIENT", InternalUtils.GetEnvironmentVariable("P4CLIENT", "", true)); } /// /// Attempts to detect source control server address from environment variables. /// /// Source control server address. private static string DetectP4Port() { string P4Port = null; // If it's not set, spawn Perforce to get the current server port setting IProcessResult Result = CommandUtils.Run(HostPlatform.Current.P4Exe, "set P4PORT", null, CommandUtils.ERunOptions.NoLoggingOfRunCommand); if (Result.ExitCode == 0) { const string KeyName = "P4PORT="; if (Result.Output.StartsWith(KeyName)) { int LastIdx = Result.Output.IndexOfAny(new char[] { ' ', '\n', '\r' }); if (LastIdx == -1) { LastIdx = Result.Output.Length; } P4Port = Result.Output.Substring(KeyName.Length, LastIdx - KeyName.Length); } } // Otherwise fallback to the uebp variables, or the default if(String.IsNullOrEmpty(P4Port)) { Logger.LogWarning("P4PORT is not set. Using perforce:1666"); P4Port = "perforce:1666"; } return P4Port; } /// /// Detects current user name. /// /// public static string DetectUserName(P4Connection Connection) { var UserName = String.Empty; var P4Result = Connection.P4("info", AllowSpew: false); if (P4Result.ExitCode != 0) { throw new AutomationException("Perforce command failed: {0}. Please make sure your P4PORT or {1} is set properly.", P4Result.Output, EnvVarNames.P4Port); } // Retrieve the P4 user name var Tags = Connection.ParseTaggedP4Output(P4Result.Output); if(!Tags.TryGetValue("User name", out UserName) || String.IsNullOrEmpty(UserName)) { if (!String.IsNullOrEmpty(UserName)) { Logger.LogWarning("Unable to retrieve perforce user name. Trying to fall back to {UserNameEnvVar} which is set to {UserName}.", EnvVarNames.User, UserName); } else { throw new AutomationException("Failed to retrieve user name."); } } return UserName; } /// /// Detects a workspace given the current user name, host name and depot path. /// /// /// User name /// Host /// Path to UAT dll, this will be checked agains the root path /// Client to use. private static P4ClientInfo DetectClient(P4Connection Connection, string UserName, string HostName, string AutomationToolDll) { Logger.LogDebug("uebp_CLIENT not set, detecting current client..."); // Check the default client. If it matches we can save any guess work. IProcessResult Result = CommandUtils.Run(HostPlatform.Current.P4Exe, "set -q P4CLIENT", null, CommandUtils.ERunOptions.NoLoggingOfRunCommand); if (Result.ExitCode == 0) { const string KeyName = "P4CLIENT="; if (Result.Output.StartsWith(KeyName)) { string ClientName = Result.Output.Substring(KeyName.Length).Trim(); P4ClientInfo ClientInfo = Connection.GetClientInfo(ClientName, true); if (ClientInfo != null && Connection.IsValidClientForFile(ClientInfo, AutomationToolDll)) { return ClientInfo; } } } // Otherwise search for all clients that match List MatchingClients = new List(); P4ClientInfo[] P4Clients = Connection.GetClientsForUser(UserName, AutomationToolDll); foreach (P4ClientInfo Client in P4Clients) { if (!String.IsNullOrEmpty(Client.Host) && String.Compare(Client.Host, HostName, true) != 0) { Logger.LogInformation("Rejecting client because of different Host {ClientName} \"{ClientHost}\" != \"{HostName}\"", Client.Name, Client.Host, HostName); continue; } MatchingClients.Add(Client); } P4ClientInfo ClientToUse = null; if (MatchingClients.Count == 0) { throw new AutomationException("No matching clientspecs found!"); } else if (MatchingClients.Count == 1) { ClientToUse = MatchingClients[0]; } else { // We may have empty host clients here, so pick the first non-empty one if possible foreach (P4ClientInfo Client in MatchingClients) { if (!String.IsNullOrEmpty(Client.Host) && String.Compare(Client.Host, HostName, true) == 0) { ClientToUse = Client; break; } } if (ClientToUse == null) { Logger.LogWarning("{NumClients} clients found that match the current host and root path. The most recently accessed client will be used.", MatchingClients.Count); ClientToUse = GetMostRecentClient(MatchingClients); } } return ClientToUse; } /// /// Given a list of clients with the same owner and root path, tries to find the most recently accessed one. /// /// List of clients with the same owner and path. /// The most recent client from the list. private static P4ClientInfo GetMostRecentClient(List Clients) { Logger.LogDebug("Detecting the most recent client."); P4ClientInfo MostRecentClient = null; var MostRecentAccessTime = DateTime.MinValue; foreach (var ClientInfo in Clients) { if (ClientInfo.Access.Ticks > MostRecentAccessTime.Ticks) { MostRecentAccessTime = ClientInfo.Access; MostRecentClient = ClientInfo; } } if (MostRecentClient == null) { throw new AutomationException("Failed to determine the most recent client in {0}", Clients[0].RootPath); } return MostRecentClient; } /// /// Detects the current changelist the workspace is synced to. /// /// /// Workspace path. /// Changelist number as a string. private static string DetectCurrentCL(P4Connection Connection, string ClientRootPath) { Logger.LogDebug("uebp_CL not set, detecting 'have' CL..."); // Retrieve the current changelist IProcessResult P4Result = Connection.P4("changes -m 1 " + CommandUtils.CombinePaths(PathSeparator.Depot, ClientRootPath, "/...#have"), AllowSpew: false); string[] CLTokens = P4Result.Output.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if(CLTokens.Length == 0) { throw new AutomationException("Unable to find current changelist (no output from 'p4 changes' command)"); } string CLString = CLTokens[1]; int CL; if(!Int32.TryParse(CLString, out CL) || CLString != CL.ToString()) { throw new AutomationException("Unable to parse current changelist from Perforce output:\n{0}", P4Result.Output); } return CLString; } /// /// Detects the current code changelist the workspace is synced to. /// /// Changelist number as a string. private static async Task DetectCurrentCodeCL(PerforceConnection Connection, int Changelist) { Logger.LogDebug("uebp_CodeCL not set, detecting last code CL..."); // Start by just testing whether the current change is a code change, so we can early out without any expensive p4 calls. int[] Changes = new[] { Changelist }; while (Changes.Length > 0) { const int NumChangesPerDescribeCall = 10; foreach (IReadOnlyList ChangeBatch in Changes.Batch(NumChangesPerDescribeCall)) { const int InitialMaxFiles = 50; List Descriptions = await Connection.DescribeAsync(DescribeOptions.None, InitialMaxFiles, ChangeBatch.ToArray()); foreach (DescribeRecord Description in Descriptions) { DescribeRecord CurrentDescription = Description; // The initial p4 describe call only queries up to InitialMaxFiles in the response. Fetching all files for large merge changelists can be prohibitively slow, // so we query an increasing number of files with the goal of earlying out as soon as we hit a code change. for (int MaxFiles = InitialMaxFiles; ;) { if (CurrentDescription.Files.Any(x => x.DepotFile != null && PerforceUtils.IsCodeFile(x.DepotFile))) { return CurrentDescription.Number; } if (CurrentDescription.Files.Count < MaxFiles) { break; } MaxFiles *= 10; CurrentDescription = await Connection.DescribeAsync(DescribeOptions.None, MaxFiles, CurrentDescription.Number); } } } // Query the last NumChanges, then split it into batches for calling p4 describe const int NumChanges = 30; List NextChanges = await Connection.GetChangesAsync(ChangesOptions.None, NumChanges, ChangeStatus.Submitted, $"//{Connection.ClientName}/...@<{Changes.Min()}"); Changes = NextChanges.Select(x => x.Number).ToArray(); } return 0; } /// /// Detects root paths for the specified client. /// /// /// Client to detect the root paths for /// Build root /// Local root /// Client root private static void DetectRootPaths(P4Connection Connection, string LocalRootPath, P4ClientInfo ThisClient, out string BuildRootPath, out string ClientRootPath) { if(!String.IsNullOrEmpty(ThisClient.Stream)) { BuildRootPath = ThisClient.Stream; ClientRootPath = String.Format("//{0}", ThisClient.Name); } else { // Figure out the build root string KnownFilePathFromRoot = CommandEnvironment.KnownFileRelativeToRoot; string KnownLocalPath = CommandUtils.MakePathSafeToUseWithCommandLine(CommandUtils.CombinePaths(PathSeparator.Slash, LocalRootPath, KnownFilePathFromRoot)); IProcessResult P4Result = Connection.P4(String.Format("files -m 1 {0}", KnownLocalPath), AllowSpew: false); string KnownFileDepotMapping = P4Result.Output; // Get the build root Logger.LogDebug("Looking for {KnownFilePathFromRoot} in {KnownFileDepotMapping}", KnownFilePathFromRoot, KnownFileDepotMapping); int EndIdx = KnownFileDepotMapping.IndexOf(KnownFilePathFromRoot, StringComparison.CurrentCultureIgnoreCase); if (EndIdx < 0) { EndIdx = KnownFileDepotMapping.IndexOf(CommandUtils.ConvertSeparators(PathSeparator.Slash, KnownFilePathFromRoot), StringComparison.CurrentCultureIgnoreCase); } // Get the root path without the trailing path separator BuildRootPath = KnownFileDepotMapping.Substring(0, EndIdx - 1); // Get the client root if (LocalRootPath.StartsWith(CommandUtils.CombinePaths(PathSeparator.Slash, ThisClient.RootPath, "/"), StringComparison.InvariantCultureIgnoreCase) || LocalRootPath == CommandUtils.CombinePaths(PathSeparator.Slash, ThisClient.RootPath)) { ClientRootPath = CommandUtils.CombinePaths(PathSeparator.Depot, String.Format("//{0}", ThisClient.Name), LocalRootPath.Substring(ThisClient.RootPath.Length)); } else { throw new AutomationException("LocalRootPath ({0}) does not start with the client root path ({1})", LocalRootPath, ThisClient.RootPath); } } } } }