// Copyright Epic Games, Inc. All Rights Reserved. using AutomationTool; using EpicGames.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using UnrealBuildTool; using Gauntlet.Utils; using System.Net; using Microsoft.Extensions.Logging; namespace Gauntlet { public enum ERoleModifier { None, Dummy, Null }; /// /// Represents a role that will be performed in an Unreal Session /// public class UnrealSessionRole { /// /// Type of role /// public UnrealTargetRole RoleType; /// /// Platform this role uses /// public UnrealTargetPlatform? Platform; /// /// Configuration this role runs in /// public UnrealTargetConfiguration Configuration; /// /// Constraints this role runs under /// public UnrealDeviceTargetConstraint Constraint; /// /// Options that this role needs /// public IConfigOption Options; /// /// Unique tag for logging purposes /// public string Tag; /// /// Command line that this role will use /// public string CommandLine { get { if (CommandLineParams == null) { CommandLineParams = new GauntletCommandLine(); } return CommandLineParams.GenerateFullCommandLine(); } set { if (CommandLineParams == null) { CommandLineParams = new GauntletCommandLine(); } CommandLineParams.ClearCommandLine(); CommandLineParams.AddRawCommandline(value, false); } } /// /// Dictionary of commandline arguments that are turned into a commandline at the end. /// For flags, leave the value set to null. Generated from the Test Role's commandline object /// and modified as needed. Then passed through to the AppConfig in UnrealBuildSource.cs /// public GauntletCommandLine CommandLineParams { get; set; } /// /// Map override to use on a server in case we don't want them all running the same map. /// public string MapOverride; /// /// List of files to copy to the device. /// public List FilesToCopy; /// /// Additional UE directories to copy from when saving artifacts /// public List AdditionalArtifactDirectories; /// /// Role device configuration /// public ConfigureDeviceHandler ConfigureDevice; /// /// Properties we require our build to have /// public BuildFlags RequiredBuildFlags; /// /// Flavor of the build /// public string RequiredFlavor; /// /// Should be represented by a null device? /// public ERoleModifier RoleModifier; /// /// Is this a dummy executable? /// public bool IsDummy() { return RoleModifier == ERoleModifier.Dummy; } /// /// Whether this role should be responsible only for installing the build and not monitoring a process. /// public bool InstallOnly { get; set; } /// /// Whether this role will launched by the test node at a later time, typically during TickTest(). By default, all roles are launched immediately. /// public bool DeferredLaunch { get; set; } /// /// Whether this role will compress screenshots produced as an artifact into a jpeg format /// public bool CompressScreenshots { get; set; } /// /// Whether this role will create a gif from screenshots produced as artifacts /// public bool CreateGifFromScreenshots { get; set; } /// /// Whether or not this role should skip the cleaning of its device's artifact between each test run /// public bool SkipCleanDeviceArtifacts { get; set; } /// /// Whether or not this role will save artifacts when running with -dev. /// These can be huge, so be cautious with using this. /// public bool ArchiveDevArtifacts { get; set; } /// /// Is this role Null? /// public bool IsNullRole() { return RoleModifier == ERoleModifier.Null; } /// /// Constructor taking limited params /// /// /// /// /// public UnrealSessionRole(UnrealTargetRole InType, UnrealTargetPlatform? InPlatform, UnrealTargetConfiguration InConfiguration, IConfigOption InOptions) : this(InType, InPlatform, InConfiguration, null, InOptions) { } /// /// Constructor taking optional params /// /// /// /// /// /// public UnrealSessionRole(UnrealTargetRole InType, UnrealTargetPlatform? InPlatform, UnrealTargetConfiguration InConfiguration, string InCommandLine = null, IConfigOption InOptions = null, bool CompressScreenshots = true, bool CreateGifFromScreenshots = true) { RoleType = InType; Platform = InPlatform; Configuration = InConfiguration; MapOverride = string.Empty; if (string.IsNullOrEmpty(InCommandLine)) { CommandLine = string.Empty; } else { CommandLine = InCommandLine; } RequiredBuildFlags = BuildFlags.None; if (Globals.IsRunningDev && !RoleType.UsesEditor()) { RequiredBuildFlags |= BuildFlags.CanReplaceExecutable; } // Enforce build flags for the platform build that support it IDeviceBuildSupport TargetBuildSupport = InterfaceHelpers.FindImplementations().Where(B => B.CanSupportPlatform(InPlatform)).FirstOrDefault(); if (TargetBuildSupport != null) { if (Globals.Params.ParseParam("bulk") && TargetBuildSupport.CanSupportBuildType(BuildFlags.Bulk)) { RequiredBuildFlags |= BuildFlags.Bulk; } else if (TargetBuildSupport.CanSupportBuildType(BuildFlags.NotBulk)) { RequiredBuildFlags |= BuildFlags.NotBulk; } if (Globals.Params.ParseParam("packaged") && TargetBuildSupport.CanSupportBuildType(BuildFlags.Packaged)) { RequiredBuildFlags |= BuildFlags.Packaged; } else if (Globals.Params.ParseParam("staged") && TargetBuildSupport.CanSupportBuildType(BuildFlags.Loose)) { RequiredBuildFlags |= BuildFlags.Loose; } } string RoleName = RoleType.ToString().ToLower(); RequiredFlavor = Globals.Params.ParseValue(RoleName + "flavor", ""); InstallOnly = false; DeferredLaunch = false; Options = InOptions; FilesToCopy = new List(); CommandLineParams = new GauntletCommandLine(); RoleModifier = ERoleModifier.None; this.CompressScreenshots = CompressScreenshots; this.CreateGifFromScreenshots = CreateGifFromScreenshots; } /// /// Debugging aid /// /// public override string ToString() { return string.Format("{0} {1} {2} {3}", Platform, Configuration, RoleType, RequiredFlavor); } } /// /// Represents an instance of a running an Unreal session. Basically an aggregate of all processes for /// all roles (clients, server, etc /// /// TODO - combine this into UnrealSession /// public class UnrealSessionInstance : IDisposable { /// /// Represents an running role in our session /// public class RoleInstance { public RoleInstance(UnrealSessionRole InRole, IAppInstance InInstance) { Role = InRole; AppInstance = InInstance; } /// /// Role that is being performed in this session /// public UnrealSessionRole Role { get; protected set; } /// /// Underlying AppInstance that is running the role /// public IAppInstance AppInstance { get; protected set; } /// /// Debugging aid /// /// public override string ToString() { return Role.ToString(); } }; /// /// All roles /// public RoleInstance[] AllRoles { get; protected set; } /// /// All running roles /// public IEnumerable RunningRoles { get { return AllRoles.Where( X => X.AppInstance != null ); } } /// /// All deferred roles /// public IEnumerable DeferredRoles { get { return DeferredRoleToAppInstall.Keys; } } /// /// All deferred roles and their associated IAppInstall /// public Dictionary DeferredRoleToAppInstall { get; protected set; } /// /// Helper for accessing all client processes. May return an empty array if no clients are involved /// public IAppInstance[] ClientApps { get { return RunningRoles.Where(R => R.Role.RoleType.IsClient()).Select(R => R.AppInstance).ToArray(); } } /// /// Helper for accessing server process. May return null if no server is involved /// public IAppInstance ServerApp { get { return RunningRoles.Where(R => R.Role.RoleType.IsServer()).Select(R => R.AppInstance).FirstOrDefault(); } } /// /// Helper for accessing editor process. May return null if no editor is involved /// public IAppInstance EditorApp { get { return RunningRoles.Where(R => R.Role.RoleType.IsEditor()).Select(R => R.AppInstance).FirstOrDefault(); } } /// /// Helper that returns true if clients are currently running /// public bool ClientsRunning { get { return ClientApps != null && ClientApps.Where(C => C.HasExited).Count() == 0; } } /// /// Helper that returns true if there's a running server /// public bool ServerRunning { get { return ServerApp != null && ServerApp.HasExited == false; } } /// /// Returns true if any of our roles are still running /// public bool IsRunningRoles { get { return RunningRoles.Any(R => R.AppInstance.HasExited == false); } } /// /// Constructor. Roles must be passed in /// /// /// public UnrealSessionInstance(RoleInstance[] InAllRoles, Dictionary InDeferredRoleToAppInstall = null) { AllRoles = InAllRoles; DeferredRoleToAppInstall = InDeferredRoleToAppInstall; } ~UnrealSessionInstance() { Dispose(false); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } Shutdown(); disposedValue = true; } } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } #endregion /// /// Returns the app install for a given role, where the role was marked as 'DeferredLaunch' /// /// public IAppInstall FindInstallForDeferredRole( UnrealSessionRole Role ) { IAppInstall AppInstall = DeferredRoleToAppInstall .Where(X => X.Key.Role == Role) .Select(X => X.Value) .FirstOrDefault(); if (AppInstall == null) { Log.Error("Cannot find derferred role {0}", Role.ToString()); } return AppInstall; } /// /// Launches a role that was previously flagged as 'DeferredLaunch'. Note that this does not handle device failure, marking problem devices etc. /// /// /// public bool LaunchDeferredRole( UnrealSessionRole Role ) { if (Role.DeferredLaunch == false) { Log.Error("Cannot start deferred role {0} because it is not marked 'DeferredLaunch'", Role.ToString()); return false; } int CurrentRoleInstanceIndex = AllRoles.FindIndex( X => X.Role == Role ); if (CurrentRoleInstanceIndex < 0) { Log.Error("Cannot start dereferred role {0} because it cannot be found among the 'all roles' list", Role.ToString()); return false; } IAppInstall CurrentInstall = FindInstallForDeferredRole(Role); bool Success = false; try { Log.Info("Starting deferred {0} on {1}", Role, CurrentInstall.Device); IAppInstance Instance = CurrentInstall.Run(); IDeviceUsageReporter.RecordStart(Instance.Device.Name, Instance.Device.Platform, IDeviceUsageReporter.EventType.Test); if (Instance != null || Globals.CancelSignalled) { // remove deferred role and update to a running role instance DeferredRoleToAppInstall.Remove(AllRoles[CurrentRoleInstanceIndex]); AllRoles[CurrentRoleInstanceIndex] = new RoleInstance(Role, Instance); } Success = true; } catch (DeviceException Ex) { Log.Error("Cannot start deferred role {0} because device {Name} threw an exception during launch. \nException={Exception}", Role.ToString(), CurrentInstall.Device, Ex.Message); Success = false; } return Success; } /// /// Shutdown the session by killing any remaining processes. /// /// public void Shutdown(bool GenerateDumpOnKill = false) { // Kill any remaining client processes if (ClientApps != null) { List RunningApps = ClientApps.Where(App =>( App != null && App.HasExited == false)).ToList(); if (RunningApps.Count > 0) { Log.Info("Shutting down {0} clients", RunningApps.Count); RunningApps.ForEach(App => { App.Kill(GenerateDumpOnKill); // Apps that are still running have timed out => fail IDeviceUsageReporter.RecordEnd(App.Device.Name, App.Device.Platform, IDeviceUsageReporter.EventType.Test, IDeviceUsageReporter.EventState.Success); }); } List ClosedApps = ClientApps.Where(App => (App != null && App.HasExited == true)).ToList(); if(ClosedApps.Count > 0) { ClosedApps.ForEach(App => { // Apps that have already exited have 'succeeded' IDeviceUsageReporter.RecordEnd(App.Device.Name, App.Device.Platform, IDeviceUsageReporter.EventType.Test, IDeviceUsageReporter.EventState.Failure); }); } } if (ServerApp != null) { if (ServerApp.HasExited == false) { Log.Info("Shutting down server"); ServerApp.Kill(GenerateDumpOnKill); } } // kill anything that's left RunningRoles.Where(R => !R.Role.InstallOnly && R.AppInstance.HasExited == false).ToList().ForEach(R => R.AppInstance.Kill(GenerateDumpOnKill)); // Wait for it all to end RunningRoles.Where(R => !R.Role.InstallOnly).ToList().ForEach(R => R.AppInstance.WaitForExit()); Thread.Sleep(3000); } } /// /// Represents the set of available artifacts available after an UnrealSessionRole has completed /// public class UnrealRoleArtifacts { /// /// Session role info that created these artifacts /// public UnrealSessionRole SessionRole { get; protected set; } /// /// AppInstance that was used to run this role /// public IAppInstance AppInstance { get; protected set; } /// /// Path to artifacts from this role (these are local and were retried from the device). /// public string ArtifactPath { get; protected set; } /// /// Path to Log from this role /// public string LogPath { get; protected set; } /// /// Constructor, all values must be provided /// /// /// /// /// public UnrealRoleArtifacts(UnrealSessionRole InSessionRole, IAppInstance InAppInstance, string InArtifactPath, string InLogPath) { SessionRole = InSessionRole; AppInstance = InAppInstance; ArtifactPath = InArtifactPath; LogPath = InLogPath; } } /// /// Helper class that understands how to launch/monitor/stop an Unreal test (clients + server) based on params contained in the test context and config /// public class UnrealSession : IDisposable { /// /// Device reservation instance of this session /// public UnrealDeviceReservation UnrealDeviceReservation { get; private set; } /// /// Source of the build that will be launched /// protected UnrealBuildSource BuildSource { get; set; } /// /// Roles that will be performed by this session /// protected UnrealSessionRole[] SessionRoles { get; set; } /// /// Running instance of this session /// public UnrealSessionInstance SessionInstance { get; protected set; } /// /// Sandbox for installed apps /// public string Sandbox { get; set; } /// /// Whether or not devices should be retained between each test iteration /// public bool ShouldRetainDevices { get; set; } [AutoParam(false)] public bool ReinstallPerPass { get; set; } /// /// Number of attempts when launching a session. /// Failed installs and runs will trigger a re-try /// public int LaunchSessionAttempts { get; set; } /// /// Number of attempts when trying to reserve devices /// public int DeviceReservationAttempts { get; set; } /// /// Number of seconds to wait between failed device reservation attempts /// public int DeviceReservationRetryTime { get; set; } /// /// Record of each ITargetDevice assigned to a given role /// public Dictionary RolesToDevices { get; private set; } /// /// Record of each UnrealAppConfig created for each role /// public Dictionary RolesToConfigs { get; private set; } /// /// Record of our installations in case we want to re-use them in a later pass /// public Dictionary RolesToInstalls { get; private set; } /// /// Constructor that takes a build source and a number of roles /// /// /// public UnrealSession(UnrealBuildSource InSource, IEnumerable InSessionRoles) { AutoParam.ApplyParamsAndDefaults(this, Globals.Params.AllArguments); BuildSource = InSource; SessionRoles = InSessionRoles.ToArray(); ShouldRetainDevices = !Globals.Params.ParseParam("ReacquireDevicesPerPass"); RolesToDevices = new Dictionary(); RolesToConfigs = new Dictionary(); RolesToInstalls = new Dictionary(); LaunchSessionAttempts = 3; DeviceReservationAttempts = 5; DeviceReservationRetryTime = 120; if (SessionRoles.Length == 0) { throw new AutomationException("No roles specified for Unreal session"); } List ValidationIssues = new List(); if (!CheckRolesArePossible(ref ValidationIssues)) { ValidationIssues.ForEach(S => Log.Error(KnownLogEvents.Gauntlet_BuildDropEvent, S)); throw new AutomationException("One or more issues occurred when validating build {0} against requested roles", InSource.BuildName); } UnrealDeviceReservation = new UnrealDeviceReservation(); } /// /// Destructor, terminates any running session /// ~UnrealSession() { Dispose(false); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } ShutdownSession(); disposedValue = true; } } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } #endregion /// /// Helper that reserves and returns a list of available devices based on the passed in roles /// /// public bool TryReserveDevices() { if (ShouldRetainDevices && HasAcquiredDevices()) { return true; } // Figure out how many of each device we need Dictionary RequiredDeviceTypes = new Dictionary(); IEnumerable RolesThatRequireDevice = SessionRoles .Where(Role => !Role.IsNullRole()) .Where(RoleNeedsDevice); // Get a count of the number of devices required for each platform foreach (UnrealSessionRole Role in RolesThatRequireDevice) { if (RequiredDeviceTypes.ContainsKey(Role.Constraint)) { ++RequiredDeviceTypes[Role.Constraint]; } else { RequiredDeviceTypes.Add(Role.Constraint, 1); } } try { return UnrealDeviceReservation.TryReserveDevices(RequiredDeviceTypes, RolesThatRequireDevice.Count(), true); } catch (Exception ex) { if (ex.InnerException != null && ex.InnerException is WebException && (ex.InnerException as WebException).Response != null && ((ex.InnerException as WebException).Response is HttpWebResponse WebResponse) && WebResponse.StatusCode == HttpStatusCode.Conflict) { // Reservation failed, usually for lack of devices // Return false to let attempt loop to kick in Log.Info(ex.Message); return false; } throw; } } public bool TryReserveDevices(int Attempts) { for (int RemainingAttempts = Attempts; RemainingAttempts > 0; --RemainingAttempts) { if(Globals.CancelSignalled) { ReleaseSessionDevices(); return false; } if(TryReserveDevices()) { return true; } else { Thread.Sleep(1000 * DeviceReservationRetryTime); } } Log.Error("Failed to reserve devices after {Attempts} attempts", Attempts); ReleaseSessionDevices(); return false; } /// /// Check that all the current roles can be performed by our build source /// /// /// bool CheckRolesArePossible(ref List Issues) { bool Success = true; foreach (var Role in SessionRoles) { if (!BuildSource.CanSupportRole(Role, ref Issues)) { Success = false; } } return Success; } /// /// Installs and launches all of our roles and returns an UnrealSessonInstance that represents the aggregate /// of all of these processes. Will perform retries if errors with devices are encountered so this is a "best /// attempt" at running with the devices provided /// /// public UnrealSessionInstance LaunchSession() { if(!Globals.UseExperimentalFeatures) { return Legacy_LaunchSession(); } // Clear any existing session from a previous iteration SessionInstance = null; // When launching, issues with devices may be encountered. // When these issues occur, those devices will be marked as problem devices and returned to the pool. // A new set of devices will then be reserved and another attempt at launching the processes will be made. // If LaunchSession() fails LaunchSessionAttempts amount of times, an exception is thrown. for (int RemainingAttempts = LaunchSessionAttempts; RemainingAttempts > 0; --RemainingAttempts) { // Reserve devices, if needed if (!TryReserveDevices(DeviceReservationAttempts)) { if(Globals.CancelSignalled) { return null; } // If device reservation fails, the device pool cannot support this launch. throw new AutomationException("Failed to acquire all devices for launch. See above for details."); } if(Globals.CancelSignalled) { return null; } if(!TryAssignDevicesToRoles()) { // If device assignment fails, reservations were likely deleted at an unexpected time. ReleaseSessionDevices(); continue; } // All roles should now be assigned a device. // Install necessary builds, clear stale test artifacts, and copy any additional files try { ReadyDevicesForSession(); } catch (Exception Ex) { if(IsOutOfSpaceException(Ex) || IsOverlayException(Ex) || Ex is not DeviceException) { RemainingAttempts = 0; ReleaseSessionDevices(); continue; } else if (RemainingAttempts > 1) { Log.Info("A new device will be selected and another attempt at launching session will be made."); } ReleaseProblemDevices(); continue; } if (Globals.CancelSignalled) { ReleaseSessionDevices(); return null; } // All roles should now be assigned device with an associated IAppInstall. // Launch all the processes! try { SessionInstance = LaunchProcesses(); } catch { if (RemainingAttempts > 1) { Log.Info("A new device will be selected and another attempt at launching session will be made."); } ReleaseProblemDevices(); continue; } if (Globals.CancelSignalled) { ReleaseSessionDevices(); return null; } return SessionInstance; } return null; } /// /// Restarts the current session (if any) /// /// public UnrealSessionInstance RestartSession() { ShutdownSession(); // AG-TODO - want to preserve device reservations here... return LaunchSession(); } /// /// Shuts down any running apps /// public void ShutdownInstance() { if (SessionInstance != null) { SessionInstance.Dispose(); SessionInstance = null; } } /// /// Shuts down the current session (if any) /// /// public void ShutdownSession() { ShutdownInstance(); if (!ShouldRetainDevices) { ReleaseSessionDevices(); } } public void ReleaseSessionDevices() { // Terminate any running apps if (SessionInstance != null && SessionInstance.RunningRoles != null) { foreach (UnrealSessionInstance.RoleInstance RunningRole in SessionInstance.RunningRoles) { Log.Info("Shutting down {0}", RunningRole.AppInstance.Device); RunningRole.AppInstance.Kill(); RunningRole.AppInstance.Device.Disconnect(); } } if (UnrealDeviceReservation == null) { return; } UnrealDeviceReservation.ReleaseDevices(); RolesToDevices.Clear(); RolesToConfigs.Clear(); RolesToInstalls.Clear(); } public void ReleaseProblemDevices() { // Terminate any running apps if (SessionInstance != null && SessionInstance.RunningRoles != null) { foreach (UnrealSessionInstance.RoleInstance RunningRole in SessionInstance.RunningRoles.Where(Role => Role.AppInstance != null)) { Log.Info("Shutting down {0}", RunningRole.AppInstance.Device); RunningRole.AppInstance.Kill(); RunningRole.AppInstance.Device.Disconnect(); } } if (UnrealDeviceReservation == null) { return; } IEnumerable ProblemDevices = UnrealDeviceReservation.ReleaseProblemDevices(); IEnumerable RolesToClear = RolesToDevices.Keys.Where(Role => ProblemDevices.Contains(RolesToDevices[Role])); Log.Info("Released problem devices..."); foreach(UnrealSessionRole Role in RolesToClear) { Log.Info("\t {DeviceName}", RolesToDevices[Role].Name); RolesToDevices.Remove(Role); RolesToConfigs.Remove(Role); RolesToInstalls.Remove(Role); } } /// /// Retrieves and saves all artifacts from the provided session role. Artifacts are saved to the destination path /// /// /// /// /// public UnrealRoleArtifacts SaveRoleArtifacts(UnrealTestContext InContext, UnrealSessionInstance.RoleInstance InRunningRole, string DestinationArtifactPath) { DirectoryInfo SourceDirectory = new DirectoryInfo(InRunningRole.AppInstance.ArtifactPath); DirectoryInfo DestinationDirectory = new DirectoryInfo(DestinationArtifactPath); DestinationDirectory.Create(); // Whether this is a Dummy, Client, Server, Editor, etc string RoleName = (InRunningRole.Role.IsDummy() ? "Dummy" : "") + InRunningRole.Role.RoleType.ToString(); // Move over any additional artifacts that a role requested (before they are removed potentially later) foreach (EIntendedBaseCopyDirectory AdditionalDirectory in InRunningRole.Role.AdditionalArtifactDirectories) { Dictionary PlatformMappings = InRunningRole.AppInstance.Device.GetPlatformDirectoryMappings(); if (PlatformMappings.ContainsKey(AdditionalDirectory)) { DirectoryInfo AdditionalSourceDirectory = new DirectoryInfo(PlatformMappings[AdditionalDirectory]); if (AdditionalSourceDirectory.Exists) { string TargetDirectory = Path.Combine(DestinationDirectory.FullName, AdditionalSourceDirectory.Name); try { SystemHelpers.CopyDirectory(AdditionalSourceDirectory.FullName, TargetDirectory); } catch (Exception Exception) { Log.Warning("Encountered an {Exception} when trying to copy additional artifact directory {BaseCopyDirectory}" + " from {AdditionalSourceDirectory} to {TargetDirectory}.", Exception, AdditionalDirectory, AdditionalDirectory, TargetDirectory); } } } } // We only want to move any other artifacts for editor data if there was a crash on a buildmachine. // Also, don't move artifacts in dev mode, because peoples saved data could be huuuuuuuge! bool IsEditorBuild = InRunningRole.Role.RoleType.UsesEditor(); bool IsBuildMachine = CommandUtils.IsBuildMachine; bool SkipArchivingAssets = (Globals.IsRunningDev && !InRunningRole.Role.ArchiveDevArtifacts) || (IsEditorBuild && (IsBuildMachine == false || InRunningRole.AppInstance.ExitCode == 0)); bool bRetainArtifacts = InContext.TestParams.ParseParam("RetainDeviceArtifacts"); // Check if we should copy artifacts if (!SkipArchivingAssets) { if (SourceDirectory.Exists) { // If a PersistentDownloadDirectory exists in the saved folder, delete it. foreach (DirectoryInfo SubDirectory in SourceDirectory.EnumerateDirectories("*", SearchOption.AllDirectories)) { if (SubDirectory.Name.Equals(EIntendedBaseCopyDirectory.PersistentDownloadDir.ToString(), StringComparison.OrdinalIgnoreCase)) { try { SubDirectory.Delete(true); } catch (Exception Exception) { Log.Info("Encountered a {Exception} when attempting to delete PersistentDownloadDirectory {Directory}. The PDD will be present in artifacts", Exception, SubDirectory); } break; } } // Perform the copy try { // Save screenshots and create a gif when not running a server if (!InRunningRole.Role.RoleType.IsServer()) { SaveScreenshots(RoleName, SourceDirectory.FullName, DestinationDirectory.FullName, InRunningRole.Role.CompressScreenshots, InRunningRole.Role.CreateGifFromScreenshots); } // Copy remaining artifacts SystemHelpers.CopyDirectory(SourceDirectory.FullName, DestinationDirectory.FullName, SystemHelpers.CopyOptions.Default, TruncateLongPathFilter); } catch (Exception Exception) { bRetainArtifacts = true; Log.Warning("Encountered an {Exception} when copying saved artifacts from {SourceDirectory} to {DestinationDirectory}. " + "Artifacts will not be saved locally, but the source artifacts will not be deleted.", Exception, SourceDirectory, DestinationDirectory); } // By default we delete the source artifacts, but we keep them if requested if (!bRetainArtifacts) { // Account for any read-only files. void SetAttributesNormal(DirectoryInfo Directory) { foreach (FileInfo File in Directory.GetFiles()) { File.Attributes = FileAttributes.Normal; } foreach (DirectoryInfo SubDirectory in Directory.GetDirectories()) { SetAttributesNormal(SubDirectory); } }; try { SetAttributesNormal(SourceDirectory); } catch { Log.Info("Could not remove the read-only attribute from {SourceDirectory}.", SourceDirectory); } try { SourceDirectory.Delete(true); } catch (Exception Exception) { Log.Info("Encountered an exception when deleting source artifacts at {SourceDirectory}. Artifacts will remain on the device.\n\tError message: {Exception}", SourceDirectory, Exception.Message); } } } else { Log.Info("Unable to find source artifact directory {SourceDirectory}", SourceDirectory.FullName); } } else { if (IsEditorBuild) { Log.Info("Skipping archival of assets for editor {Role}", RoleName); } else if (Globals.IsRunningDev) { Log.Info("Skipping archival of assets for dev build"); } } // Now write the role's log file string ArtifactLogFilePath = Path.GetFullPath(Path.Combine(DestinationDirectory.FullName, RoleName + "Output.log")); try { if (!InRunningRole.AppInstance.WriteOutputToFile(ArtifactLogFilePath)) { ArtifactLogFilePath = string.Empty; } } catch (Exception Ex) { string Message = "Encountered an {0} when attempting to write the {1} process log. The log will not be present in the artifacts.\n {2}"; Log.Warning(Message, Ex.GetType().Name, RoleName, Ex.Message); ArtifactLogFilePath = string.Empty; } if (!string.IsNullOrEmpty(ArtifactLogFilePath)) { Log.Info($"Wrote {RoleName} Log to {ArtifactLogFilePath}"); // On build machines, copy all role logs to Horde. if (IsBuildMachine && Horde.IsHordeJob) { // Extract the log path portion that includes the Gauntlet test name. ie: UE.BootTest(Win64_Test_Client)\Client\ClientOutput.log // That is to handle situation where multiple tests are run within the same Gauntlet Session and logs get overwritten. string LogName = ArtifactLogFilePath.Replace(Path.GetFullPath(InContext.Options.LogDir), "").TrimStart(Path.DirectorySeparatorChar); if (Path.IsPathFullyQualified(LogName)) { // The path was expected to be relative to LogDir, however it appeared to be an absolute path. // So we revert to default behavior and save the log directly in UAT log folder. LogName = RoleName + "Output.log"; } string HordeLogFilePath = Path.GetFullPath(Path.Combine(CommandUtils.CmdEnv.LogFolder, LogName)); Log.Verbose($"Copy log for Horde to {HordeLogFilePath}"); string TargetDirectry = Path.GetDirectoryName(HordeLogFilePath); if (!Directory.Exists(TargetDirectry)) { Directory.CreateDirectory(TargetDirectry); } File.Copy(ArtifactLogFilePath, HordeLogFilePath, true); } } // TODO REMOVEME- this should go elsewhere, likely a utile that can be called or inserted by relevant test nodes. SavePSOs(InContext, InRunningRole, DestinationDirectory.FullName); // END REMOVEME // Save the Artifact filepath return new UnrealRoleArtifacts(InRunningRole.Role, InRunningRole.AppInstance, DestinationDirectory.FullName, ArtifactLogFilePath); } /// /// Saves all artifacts from the provided session to the specified output path. /// /// /// /// /// public IEnumerable SaveRoleArtifacts(UnrealTestContext Context, UnrealSessionInstance TestInstance, string OutputPath) { int DummyClientCount = 0; Dictionary RoleCounts = new Dictionary(); List AllArtifacts = new List(); foreach (UnrealSessionInstance.RoleInstance App in TestInstance.RunningRoles) { string RoleName = (App.Role.IsDummy() ? "Dummy" : "") + App.Role.RoleType.ToString(); string FolderName = RoleName; int RoleCount = 1; if (App.Role.IsDummy()) { DummyClientCount++; RoleCount = DummyClientCount; } else { if (!RoleCounts.ContainsKey(App.Role.RoleType)) { RoleCounts.Add(App.Role.RoleType, 1); } else { RoleCounts[App.Role.RoleType]++; } RoleCount = RoleCounts[App.Role.RoleType]; } if (RoleCount > 1) { FolderName += string.Format("_{0:00}", RoleCount); } string DestPath = Path.Combine(OutputPath, FolderName); if (!App.Role.IsNullRole() && !App.Role.InstallOnly) { Log.VeryVerbose("Calling SaveRoleArtifacts, Role: {0} Artifact Path: {1}", App.ToString(), App.AppInstance.ArtifactPath); UnrealRoleArtifacts Artifacts = null; ITargetDevice device = App.AppInstance.Device; IDeviceUsageReporter.RecordStart(device.Name, device.Platform, IDeviceUsageReporter.EventType.SavingArtifacts); try { Artifacts = SaveRoleArtifacts(Context, App, DestPath); } catch (Exception SaveArtifactsException) { // Caught an exception -> report failure IDeviceUsageReporter.RecordEnd(device.Name, device.Platform, IDeviceUsageReporter.EventType.SavingArtifacts, IDeviceUsageReporter.EventState.Failure); // Retry once only, after rebooting device, if artifacts couldn't be saved. if (SaveArtifactsException.Message.Contains("A retry should be performed")) { Log.Info("Rebooting device and retrying save role artifacts once."); App.AppInstance.Device.Reboot(); Artifacts = SaveRoleArtifacts(Context, App, DestPath); } else { // Pass exception to the surrounding try/catch in UnrealTestNode while preserving the original callstack throw; } } // Did not catch -> successful reporting IDeviceUsageReporter.RecordEnd(device.Name, device.Platform, IDeviceUsageReporter.EventType.SavingArtifacts, IDeviceUsageReporter.EventState.Success); if (Artifacts != null) { AllArtifacts.Add(Artifacts); } } else { Log.Verbose("Skipping SaveRoleArtifacts for Null Role: {0}", App.ToString()); } } return AllArtifacts; } private UnrealSessionInstance Legacy_LaunchSession() { SessionInstance = null; // The number of retries when launching session to avoid an endless loop if package can't be installed, network timeouts, etc int SessionRetries = 2; // tries to find devices and launch our session. Will loop until we succeed, we run out of devices/retries, or // something fatal occurs.. while (SessionInstance == null && Globals.CancelSignalled == false) { int ReservationRetries = 5; int ReservationRetryWait = 120; IEnumerable RolesNeedingDevices = SessionRoles.Where(R => R.IsNullRole() == false); while (UnrealDeviceReservation.ReservedDevices.Count() < RolesNeedingDevices.Count()) { // get devices TryReserveDevices(); if (Globals.CancelSignalled) { break; } // if we failed to get enough devices, show a message and wait if (UnrealDeviceReservation.ReservedDevices.Count() != SessionRoles.Count()) { if (ReservationRetries == 0) { throw new AutomationException("Unable to acquire all devices for test."); } Log.Info("\nUnable to find enough device(s). Waiting {0} secs (retries left={1})\n", ReservationRetryWait, --ReservationRetries); Thread.Sleep(ReservationRetryWait * 1000); } } if (Globals.CancelSignalled) { return null; } Dictionary InstallsToRoles = new Dictionary(); Dictionary InstallsToConfig = new Dictionary(); // create a copy of our list IEnumerable DevicesToInstallOn = UnrealDeviceReservation.ReservedDevices.ToArray(); bool InstallSuccess = true; // sort by constraints, so that we pick constrained devices first List SortedRoles = SessionRoles.OrderBy(R => R.Constraint.IsIdentity() ? 1 : 0).ToList(); // first install all roles on these devices foreach (UnrealSessionRole Role in SortedRoles) { ITargetDevice Device = null; if (Role.IsNullRole() == false) { Device = DevicesToInstallOn.Where(D => D.IsConnected && D.Platform == Role.Platform && (Role.Constraint.IsIdentity() || DevicePool.Instance.GetConstraint(D) == Role.Constraint)).First(); DevicesToInstallOn = DevicesToInstallOn.Where(D => D != Device); } else { Device = new TargetDeviceNull(string.Format("Null{0}", Role.RoleType)); } IEnumerable OtherRoles = SortedRoles.Where(R => R != Role); // create a config from the build source (this also applies the role options) UnrealAppConfig AppConfig = BuildSource.CreateConfiguration(Role, OtherRoles); // todo - should this be elsewhere? AppConfig.Sandbox = Sandbox; IAppInstall Install = null; if (RolesToInstalls == null || !RolesToInstalls.ContainsKey(Role) || ReinstallPerPass) { // Tag the device for report result if (BuildHostPlatform.Current.Platform != Device.Platform) { AppConfig.CommandLineParams.Add("DeviceTag", Device.Name); } IDeviceUsageReporter.RecordStart(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Device, IDeviceUsageReporter.EventState.Success); IDeviceUsageReporter.RecordStart(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Success, BuildSource.BuildName); try { // Make sure the artifact paths are clean before installation if(!Role.SkipCleanDeviceArtifacts) { Device.CleanArtifacts(AppConfig); } Install = Device.InstallApplication(AppConfig); IDeviceUsageReporter.RecordEnd(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Success); } catch (Exception Ex) { InstallSuccess = false; string ErrorMessage = string.Format("Encountered error setting up device {0} for role {1}. {2}", Device, Role, Ex); if (ErrorMessage.Contains("not enough space") && Device.Platform == BuildHostPlatform.Current.Platform) { Log.Error(ErrorMessage); // If on desktop platform, we are not retrying. // It is unlikely that space is going to be made and InstallBuildParallel has marked the build path as problematic. SessionRetries = 0; break; } // Warn, ignore the device, and do not continue if (Ex is DeviceException) { UnrealDeviceReservation.MarkProblemDevice(Device, ErrorMessage); } else { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, ErrorMessage); } IDeviceUsageReporter.RecordEnd(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Failure); break; } if (Globals.CancelSignalled) { break; } // Device has app installed, give role a chance to configure device Role.ConfigureDevice?.Invoke(Device); InstallsToRoles[Install] = Role; InstallsToConfig[Install] = AppConfig; if (ReinstallPerPass) { RolesToInstalls[Role] = Install; } } else { Install = RolesToInstalls[Role]; InstallsToRoles[Install] = Role; InstallsToConfig[Install] = AppConfig; Log.Info("Using previous install of {0} on {1}", Install.Name, Install.Device.Name); } } if (InstallSuccess == false) { ReleaseSessionDevices(); if (SessionRetries == 0) { throw new AutomationException("Unable to install application for session."); } Log.Info("\nUnable to install application for session (retries left={0})\n", --SessionRetries); } if (InstallSuccess && Globals.CancelSignalled == false) { List AllRoles = new List(); Dictionary DeferredRoleToAppInstall = new Dictionary(); // Now try to run all installs on their devices foreach (var InstallRoleKV in InstallsToRoles) { IAppInstall CurrentInstall = InstallRoleKV.Key; if (InstallRoleKV.Value.InstallOnly) { AllRoles.Add(new UnrealSessionInstance.RoleInstance(InstallRoleKV.Value, null)); continue; } if (InstallRoleKV.Value.DeferredLaunch) { UnrealSessionInstance.RoleInstance DeferredRoleInstance = new UnrealSessionInstance.RoleInstance(InstallRoleKV.Value, null); DeferredRoleToAppInstall.Add(DeferredRoleInstance, CurrentInstall); AllRoles.Add(DeferredRoleInstance); continue; } bool Success = false; try { Log.Info("Starting {0} on {1}", InstallRoleKV.Value, CurrentInstall.Device); IAppInstance Instance = CurrentInstall.Run(); IDeviceUsageReporter.RecordStart(Instance.Device.Name, Instance.Device.Platform, IDeviceUsageReporter.EventType.Test); if (Instance != null || Globals.CancelSignalled) { AllRoles.Add(new UnrealSessionInstance.RoleInstance(InstallRoleKV.Value, Instance)); } Success = true; } catch (DeviceException Ex) { // mark that device as a problem Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to start build on {DeviceName}. Marking as problem device and retrying with new set", CurrentInstall.Device.Name); UnrealDeviceReservation.MarkProblemDevice(CurrentInstall.Device, $"Device threw an exception during launch.\nException={Ex.Message}"); Success = false; } if (Success == false) { // terminate anything that's running foreach (UnrealSessionInstance.RoleInstance RunningRole in AllRoles.Where(X => X.AppInstance != null)) { Log.Info("Shutting down {DeviceName}", RunningRole.AppInstance.Device.Name); RunningRole.AppInstance.Kill(); RunningRole.AppInstance.Device.Disconnect(); } ReleaseSessionDevices(); if (SessionRetries == 0) { throw new AutomationException("Unable to start application for session, see log for details."); } Log.Info("\nUnable to start application for session (retries left={0})\n", --SessionRetries); break; // do not continue loop } } if (AllRoles.Count() == SessionRoles.Count()) { SessionInstance = new UnrealSessionInstance(AllRoles.ToArray(), DeferredRoleToAppInstall); } } } return SessionInstance; } /// /// Returns true if the number of reserved devices matches the number on non-null session roles /// private bool HasAcquiredDevices() { IEnumerable RolesThatRequireDevice = SessionRoles.Where(Role => !Role.IsNullRole()); return UnrealDeviceReservation != null && UnrealDeviceReservation.ReservedDevices != null && UnrealDeviceReservation.ReservedDevices.Count() == RolesThatRequireDevice.Count(); } /// /// Returns true if the provided session role has not yet been assigned a device /// private bool RoleNeedsDevice(UnrealSessionRole Role) { return !(RolesToDevices.ContainsKey(Role) && RolesToDevices[Role] != null); } /// /// Returns true if the provided session role has not yet had an install performed on it's assigned device /// private bool RoleNeedsInstall(UnrealSessionRole Role) { return ReinstallPerPass || !(RolesToConfigs.ContainsKey(Role) && RolesToConfigs[Role] != null) || !(RolesToInstalls.ContainsKey(Role) && RolesToInstalls[Role] != null); } /// /// Returns true if the provided target device matches the constraint requested by the session role /// private bool DeviceMatchesRoleConstraint(UnrealSessionRole Role, ITargetDevice Device) { bool bRoleMatchesConstraint = Role.Constraint.Equals(DevicePool.Instance.GetConstraint(Device)); return Device.IsConnected && Device.Platform == Role.Platform && Role.Constraint.IsIdentity() || bRoleMatchesConstraint; } /// /// From the existing reserved device pool, assign each role a device that matches it's requested constraint /// This will cache a map of each role and the target device it's using in this UnrealSession /// private bool TryAssignDevicesToRoles() { // Order by constraint. This ensures roles with constraints have their devices selected first. IEnumerable RolesSortedByConstraint = SessionRoles.OrderBy(R => R.Constraint.IsIdentity() ? 1 : 0); IEnumerable ReservedDevices = UnrealDeviceReservation.ReservedDevices; foreach (UnrealSessionRole Role in RolesSortedByConstraint) { if (RoleNeedsDevice(Role)) { ITargetDevice DeviceToAssign = null; if (Role.IsNullRole()) { DeviceToAssign = new TargetDeviceNull($"Null{Role.RoleType}"); } else { try { DeviceToAssign = ReservedDevices.Where(Device => DeviceMatchesRoleConstraint(Role, Device)).First(); IDeviceUsageReporter.RecordStart(DeviceToAssign.Name, DeviceToAssign.Platform, IDeviceUsageReporter.EventType.Device, IDeviceUsageReporter.EventState.Success); } catch (Exception Ex) { Log.Warning("Failed to assign a reserved device to role {Role}. " + "This usually means devices were unexpectedly released mid session\n{Exception}", Role, Ex); return false; } } RolesToDevices.Add(Role, DeviceToAssign); ReservedDevices = ReservedDevices.Except(Enumerable.Repeat(DeviceToAssign, 1)); } } return true; } /// /// Prepares each device for launch by performing the following /// - Install builds /// - Clean up old artifacts /// - Copy additional files requested by a test /// - Specific role configurations /// This will create and cache both an UnrealAppConfig and an IAppInstall for future reference /// private void ReadyDevicesForSession() { foreach(UnrealSessionRole Role in SessionRoles) { UnrealAppConfig AppConfig = null; ITargetDevice Device = RolesToDevices[Role]; if (Globals.CancelSignalled) { return; } try { if (RoleNeedsInstall(Role)) { RolesToConfigs.Remove(Role); RolesToInstalls.Remove(Role); // Create the app config IEnumerable OtherRoles = SessionRoles.Where(Other => Other != Role); AppConfig = BuildSource.CreateConfiguration(Role, OtherRoles); AppConfig.Sandbox = Sandbox; AppConfig.CommandLineParams.AddUnique("DeviceTag", Device.Name); RolesToConfigs.Add(Role, AppConfig); if (AppConfig.FullClean) { Log.Info("Fully cleaning device before install..."); Device.FullClean(); } if (!Role.SkipCleanDeviceArtifacts) { Device.CleanArtifacts(AppConfig); } if (AppConfig.SkipInstall) { Log.Info("Skipping install due to SkipInstall"); } else { // Telemetry DateTimeStopwatch Stopwatch = DateTimeStopwatch.Start(); Log.Info("Installing {BuildName} of type {BuildType} to {DeviceName}...", BuildSource.BuildName, AppConfig.Build.GetType().Name, Device.Name); IDeviceUsageReporter.RecordStart(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Success, BuildSource.BuildName); try { Device.InstallBuild(AppConfig); IDeviceUsageReporter.RecordEnd(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Success); Log.Info("Installation completed in {InstallTime}", GetInstallTime(Stopwatch.ElapsedTime)); } catch { IDeviceUsageReporter.RecordEnd(Device.Name, Device.Platform, IDeviceUsageReporter.EventType.Install, IDeviceUsageReporter.EventState.Failure); throw; } } IAppInstall Install = Device.CreateAppInstall(AppConfig); RolesToInstalls.Add(Role, Install); } else { AppConfig = RolesToConfigs[Role]; if (!Role.SkipCleanDeviceArtifacts) { Device.CleanArtifacts(AppConfig); } } if (RolesToConfigs[Role].Build.SupportsAdditionalFileCopy) { Device.CopyAdditionalFiles(RolesToConfigs[Role].FilesToCopy); } Role.ConfigureDevice?.Invoke(Device); } catch(Exception Ex) { string Message = $"Encountered {Ex.GetType()} when creating installation.\n{Ex.Message}"; bool IsHostPlatform = Device.Platform == BuildHostPlatform.Current.Platform; EventId EventType = IsHostPlatform ? KnownLogEvents.Gauntlet : KnownLogEvents.Gauntlet_DeviceEvent; if (IsOutOfSpaceException(Ex) && IsHostPlatform) { // If on desktop platform, we are not retrying. // It is unlikely that space is going to be made and InstallBuildParallel has marked the build path as problematic. Log.Error(EventType, Message); throw; } else if (IsOverlayException(Ex)) { // Errors with Overlay executables are caused by missing files or improper setup, inform user and exit early Log.Error(EventType, "Device {DeviceName} {Message}", Device.Name, Message); throw; } else if (Ex is DeviceException && !IsHostPlatform) { UnrealDeviceReservation.MarkProblemDevice(Device, Message); } else { Log.Warning(EventType, Message); } throw; // Can consider not throwing here - this would let every device complete setup before releasing the problem devices } } } /// /// Launches the Unreal Engine processes for each role requested by this session. /// Roles marked InstallOnly will not have a process launched. /// Roles marked DeferredLaunch will not have a process launched. /// Deferred roles can be launched at anytime (usually in TickTest()) by calling UnrealSessionInstance.LaunchDeferredRole /// private UnrealSessionInstance LaunchProcesses() { List RoleInstances = new(); Dictionary DeferredRolesToInstalls = new(); foreach (KeyValuePair RoleInstall in RolesToInstalls) { UnrealSessionRole Role = RoleInstall.Key; IAppInstall Install = RoleInstall.Value; UnrealAppConfig Config = RolesToConfigs[Role]; // InstallOnly roles don't execute a process if (Role.InstallOnly) { RoleInstances.Add(new UnrealSessionInstance.RoleInstance(Role, null)); continue; } // DeferredLaunch roles don't immediately execute a process. // We cache deferred roles so users can launch the process at the desired time. else if (Role.DeferredLaunch) { UnrealSessionInstance.RoleInstance DeferredRoleInstance = new(Role, null); RoleInstances.Add(DeferredRoleInstance); DeferredRolesToInstalls.Add(DeferredRoleInstance, Install); continue; } try { Log.Info("Launching {Install} on {DeviceName}", Install, RolesToDevices[Role].Name); IAppInstance AppInstance = Install.Run(); if (AppInstance == null) { throw new AutomationException("Failed to create an IAppInstance after attempting Run() on {Install}", Install); } else { RoleInstances.Add(new UnrealSessionInstance.RoleInstance(Role, AppInstance)); } } catch(Exception Ex) { // Kill any processes that were started foreach (UnrealSessionInstance.RoleInstance Instance in RoleInstances.Where(Role => Role.AppInstance != null)) { Log.Info("Shutting down {AppInstance}", Instance.AppInstance); Instance.AppInstance.Kill(); } string WarningMessage = $"Encountered {Ex.GetType()} when attempting to run install {Ex.Message}"; if (Ex is DeviceException) { UnrealDeviceReservation.MarkProblemDevice(Install.Device, WarningMessage); } else { Log.Warning(WarningMessage); } throw; } } return new UnrealSessionInstance(RoleInstances.ToArray(), DeferredRolesToInstalls); } private void SaveScreenshots(string RoleName, string SourceDirectory, string DestinationDirectory, bool bCompressImages, bool bCreateGif) { try { string ScreenshotPath = PathUtils.FindRelevantPath(BasePath: SourceDirectory, "Screenshots"); if (string.IsNullOrEmpty(ScreenshotPath) || !Directory.Exists(ScreenshotPath)) { Log.Verbose("Could not locate Screenshots subdirectory in artifact path {ArtifactPath}. Skipping GIF creation", SourceDirectory); return; } // Image transformation relies on the image being directly on the host PC // This means it can't be on a remote device, or in a network storage. // The screenshots directory will be copied to a temp directory for the transformations, and then moved to the final artifact destination. DirectoryInfo ScreenshotDirectory = new(ScreenshotPath); DirectoryInfo ImageStage = new(Path.Combine(Path.GetTempPath(), "ImageStage", RoleName)); if (ImageStage.Exists) { ImageStage.Delete(true); } // Copy the images to a the staging directory SystemHelpers.CopyDirectory(ScreenshotPath, ImageStage.FullName, SystemHelpers.CopyOptions.Mirror); // Creates an enumerable containing both the root directory and any sub directories IEnumerable ImageDirectories = ImageStage .EnumerateDirectories() .Concat(Enumerable.Repeat(ImageStage, 1)); // Compress the images and convert them to a gif foreach (DirectoryInfo ImageDirectory in ImageDirectories) { FileInfo[] Files = ImageDirectory.GetFiles(); if (Files.Any()) { if (bCompressImages) { Image.ConvertImages(ImageDirectory.FullName, ImageDirectory.FullName, "jpg", true); } string GifPath = GenerateNotTakenFilePath(Path.Combine(DestinationDirectory, RoleName + "Test.gif")); if (bCreateGif) { if (Image.SaveImagesAsGif(ImageDirectory.FullName, GifPath)) { Log.Info("Saved gif to {0}", GifPath); } } } } // Now, copy the compressed images in the temp staged directory to the desired artifact destination string RelativeScreenshotDirectory = ScreenshotPath.Replace(SourceDirectory, string.Empty).Trim('\\').Trim('/'); string ScreenshotDestination = Path.Combine(DestinationDirectory, RelativeScreenshotDirectory); SystemHelpers.CopyDirectory(ImageStage.FullName, ScreenshotDestination, SystemHelpers.CopyOptions.Mirror); // Delete the source screenshot directory so they aren't duplicated in the following artifact copy ScreenshotDirectory.Delete(true); ImageStage.Delete(true); } catch (Exception Ex) { Log.Info("Failed to downsize and gif-ify images! {0}", Ex.Message); } } private void SavePSOs(UnrealTestContext InContext, UnrealSessionInstance.RoleInstance InRunningRole, string DestSavedDir) { if (InRunningRole.Role.RoleType.IsServer()) { return; } if (!InContext.Options.LogPSO) { return; } if (!Directory.Exists(DestSavedDir)) { try { Directory.CreateDirectory(DestSavedDir); } catch (Exception Ex) { Log.Info($"Archive path '{DestSavedDir}' was not found!\n{Ex.Message}"); return; } } // Copy over PSOs try { foreach (string ThisFile in CommandUtils.FindFiles_NoExceptions(true, "*.rec.upipelinecache", true, DestSavedDir)) { bool Copied = false; string JustFile = Path.GetFileName(ThisFile); if (!JustFile.StartsWith("++")) { continue; } string[] Parts = JustFile.Split(new Char[] { '+', '-' }).Where(A => A != "").ToArray(); if (Parts.Count() >= 2) { string ProjectName = Parts[0].ToString(); string BuildRoot = CommandUtils.CombinePaths(CommandUtils.RootBuildStorageDirectory()); string SrcBuildPath = CommandUtils.CombinePaths(BuildRoot, ProjectName); string SrcBuildPath2 = CommandUtils.CombinePaths(BuildRoot, ProjectName.Replace("Game", "").Replace("game", "")); if (!CommandUtils.DirectoryExists(SrcBuildPath)) { SrcBuildPath = SrcBuildPath2; } if (CommandUtils.DirectoryExists(SrcBuildPath)) { var JustBuildFolder = JustFile.Replace("-" + Parts.Last(), ""); string PlatformStr = InRunningRole.Role.Platform.ToString(); string SrcCLMetaPath = CommandUtils.CombinePaths(SrcBuildPath, JustBuildFolder, PlatformStr, "MetaData"); if (CommandUtils.DirectoryExists(SrcCLMetaPath)) { string SrcCLMetaPathCollected = CommandUtils.CombinePaths(SrcCLMetaPath, "CollectedPSOs"); if (!CommandUtils.DirectoryExists(SrcCLMetaPathCollected)) { Log.Info("Creating Directory {0}", SrcCLMetaPathCollected); CommandUtils.CreateDirectory(SrcCLMetaPathCollected); } if (CommandUtils.DirectoryExists(SrcCLMetaPathCollected)) { string DestFile = CommandUtils.CombinePaths(SrcCLMetaPathCollected, JustFile); CommandUtils.CopyFile_NoExceptions(ThisFile, DestFile, true); if (CommandUtils.FileExists(true, DestFile)) { Log.Info("Deleting local file, copied to {0}", DestFile); CommandUtils.DeleteFile_NoExceptions(ThisFile, true); Copied = true; } } } } } if (!Copied) { Log.Warning("Could not find anywhere to put this file {0}", JustFile); } } } catch (Exception Ex) { Log.Info("Failed to copy upipelinecaches to the network {0}", Ex); } } private string GenerateNotTakenFilePath(string DesiredPath) { string ResultPath = null; FileInfo PotentialPathFileInfo = new FileInfo(DesiredPath); for (int NumericPostfix = 0; (string.IsNullOrEmpty(ResultPath)) && (NumericPostfix < int.MaxValue); NumericPostfix++) { string PotentialPath = DesiredPath; if (NumericPostfix > 0) { PotentialPath = Path.Combine( PotentialPathFileInfo.DirectoryName, string.Format("{0}_{1}", Path.GetFileNameWithoutExtension(PotentialPathFileInfo.Name), NumericPostfix)); if (!string.IsNullOrEmpty(PotentialPathFileInfo.Extension)) { PotentialPath += PotentialPathFileInfo.Extension; } } bool PathIsTaken = File.Exists(PotentialPath); if (PathIsTaken) { Log.VeryVerbose("File already exists at {0}", PotentialPath); } else { ResultPath = PotentialPath; } } if (string.IsNullOrEmpty(ResultPath)) { throw new AutomationException("Cannot generate not taken file path for the path {0}", DesiredPath); } return ResultPath; } /// /// Filter that Truncate long file paths such as CrashReporter files. //UECC-Windows-F0DD9BB04C3C9250FAF39D8AB4A88556// /// These are particularly problematic with testflights which append a long random name to the destination folder, /// easily pushing past 260 chars. /// /// /// private string TruncateLongPathFilter(string LongFilePath) { Dictionary LongCrashReporterStringToIndex = new Dictionary(); Match RegexMatch = Regex.Match(LongFilePath, @"((?i)UECC)-.+-([\dA-Fa-f]+)"); if (RegexMatch.Success) { string LongString = RegexMatch.Groups[2].ToString(); if (!LongCrashReporterStringToIndex.ContainsKey(LongString)) { LongCrashReporterStringToIndex[LongString] = LongCrashReporterStringToIndex.Keys.Count.ToString("D2"); } string ShortSring = LongCrashReporterStringToIndex[LongString]; LongFilePath = LongFilePath.Replace(LongString, ShortSring); } return LongFilePath; } private string GetInstallTime(TimeSpan Time) { string Hours = Time.Hours > 0 ? string.Format("{0} hrs, ", Time.Hours) : string.Empty; string Minutes = Time.Minutes > 0 ? string.Format("{0} mins, ", Time.Minutes) : string.Empty; string Seconds = string.Format("{0} secs", Time.Seconds); return Hours + Minutes + Seconds; } private bool IsOutOfSpaceException(Exception Ex) { return Ex.Message.Contains("not enough space", StringComparison.OrdinalIgnoreCase); } private bool IsOverlayException(Exception Ex) { return Ex.Message.Contains("Overlay Error", StringComparison.OrdinalIgnoreCase); } } }