// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using AutomationTool; using AutomationTool.DeviceReservation; using UnrealBuildTool; using System.Text.RegularExpressions; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using EpicGames.Core; using System.Data; using Gauntlet.Utils; using System.Reflection; namespace Gauntlet { /// /// Device performance specification /// public enum EPerfSpec { Unspecified, Minimum, Recommended, High }; /// /// Information that defines a device /// public class DeviceDefinition { public string Name { get; set; } public string Address { get; set; } public string DeviceData { get; set; } // legacy - remove! [JsonConverter(typeof(UnrealTargetPlatformConvertor))] public UnrealTargetPlatform Type { get; set; } [JsonConverter(typeof(UnrealTargetPlatformConvertor))] public UnrealTargetPlatform? Platform { get; set; } public EPerfSpec PerfSpec { get; set; } public string Model { get; set; } = string.Empty; public string Available { get; set; } public bool RemoveOnShutdown { get; set; } public override string ToString() { return string.Format("{0} @ {1}. Platform={2} Model={3}", Name, Address, Platform, string.IsNullOrEmpty(Model) ? "Unspecified" : Model); } } /// /// Device target constraint, can be expanded for specifying installed RAM, OS version, etc /// public class UnrealDeviceTargetConstraint : IEquatable { public readonly UnrealTargetPlatform? Platform; public readonly EPerfSpec PerfSpec; public readonly string Model; public readonly string DeviceName; public UnrealDeviceTargetConstraint(UnrealTargetPlatform? Platform, EPerfSpec PerfSpec = EPerfSpec.Unspecified, string Model = null, string DeviceName = null) { this.Platform = Platform; this.PerfSpec = PerfSpec; this.Model = Model == null ? string.Empty : Model; this.DeviceName = DeviceName == null ? string.Empty : DeviceName; } /// /// Tests whether the constraint is identity, ie. unconstrained /// public bool IsIdentity() { return (PerfSpec == EPerfSpec.Unspecified) && (Model == string.Empty) && (DeviceName == string.Empty); } /// /// Check whether device satisfies the constraint /// public bool Check(ITargetDevice Device) { return Platform == Device.Platform && (IsIdentity() || this == DevicePool.Instance.GetConstraint(Device)); } public bool Check(DeviceDefinition DeviceDef) { if (Platform != DeviceDef.Platform) { return false; } if (IsIdentity()) { return true; } bool ModelMatch = Model == string.Empty ? true : Model.Equals(DeviceDef.Model, StringComparison.InvariantCultureIgnoreCase); bool PerfMatch = (PerfSpec == EPerfSpec.Unspecified) ? true : PerfSpec == DeviceDef.PerfSpec; bool NameMatch = DeviceName == string.Empty? true : DeviceName.Equals(DeviceDef.Name, StringComparison.InvariantCultureIgnoreCase); return ModelMatch && PerfMatch && NameMatch; } public bool Equals(UnrealDeviceTargetConstraint Other) { if (ReferenceEquals(Other, null)) { throw new AutomationException("Comparing null target constraint"); } if (ReferenceEquals(this, Other)) { return true; } return Other.Platform == Platform && Other.Model.Equals(Model, StringComparison.InvariantCultureIgnoreCase) && Other.DeviceName.Equals(DeviceName, StringComparison.InvariantCultureIgnoreCase) && Other.PerfSpec == PerfSpec; } public override bool Equals(object Obj) { if (ReferenceEquals(Obj, null)) { throw new AutomationException("Comparing null target constraint"); } if (ReferenceEquals(this, Obj)) { return true; } if (Obj.GetType() != typeof(UnrealDeviceTargetConstraint)) { return false; } return Equals((UnrealDeviceTargetConstraint)Obj); } public static bool operator ==(UnrealDeviceTargetConstraint C1, UnrealDeviceTargetConstraint C2) { if (ReferenceEquals(C1, null) || ReferenceEquals(C2, null)) { throw new AutomationException("Comparing null target constraint"); } return C1.Equals(C2); } public static bool operator !=(UnrealDeviceTargetConstraint C1, UnrealDeviceTargetConstraint C2) { return !(C1 == C2); } public override string ToString() { string Value = Platform.ToString(); if (PerfSpec != EPerfSpec.Unspecified) { Value = string.Format("{0}:{1}", Value, PerfSpec.ToString()); } if(Model != string.Empty) { Value = string.Format("{0}:{1}", Value, Model); } if (DeviceName != string.Empty) { Value = string.Format("{0}:{1}", Value, DeviceName); } return Value; } /// /// Format to string the device constraint including its identify /// /// public string FormatWithIdentifier() { return string.Format("{0}:{1}", Platform, Model == string.Empty ? PerfSpec.ToString() : Model); } public override int GetHashCode() { return ToString().GetHashCode(); } } /// /// Device marked as having a problem /// public struct ProblemDevice { public ProblemDevice(string Name, UnrealTargetPlatform Platform) { this.Name = Name; this.Platform = Platform; } public string Name; public UnrealTargetPlatform Platform; } /// /// Singleton class that's responsible for providing a list of devices and reserving them. Code should call /// EnumerateDevices to build a list of desired devices, which must then be reserved by calling ReserveDevices. /// Once done ReleaseDevices should be called. /// /// These reservations exist at the process level and we rely on the device implementation to provide arbitrage /// between difference processes and machines /// /// public class DevicePool : IDisposable { /// /// Access to our singleton /// public static DevicePool Instance { get { if (_Instance == null || _Instance.bDisposed) { if (_Instance != null) { Log.Info("DevicePool has been disposed. Reinitilizing with a new instance."); } _Instance = new DevicePool(); } return _Instance; } private set { _Instance = value; } } public static bool SkipInstall; public static bool FullClean; public static bool DeviceReservationBlock; public static bool? IsInstallStep; /// /// Active pool instance /// private static DevicePool _Instance; /// /// Object used for locking access to internal data /// private object LockObject = new object(); /// /// List of all provisioned devices that are can be claimed /// private List AvailableDevices = new List(); /// /// List of all provisioned devices that have been claimed /// private List ClaimedDevices = new List(); /// /// List of all provisioned devices that were not provided by a reservation service /// private List LocalDevices => AvailableDevices .Union(ClaimedDevices) .Except(ReservationServices.SelectMany(Service => Service.ReservedDevices)) .ToList(); /// /// List of all enabled reservation services /// private List ReservationServices = new List(); /// /// List of platforms we've had devices for /// private HashSet UsedPlatforms = new HashSet(); /// /// List of device definitions that can be provisioned on demand /// private List UnprovisionedDevices = new List(); /// /// List of definitions that failed to provision /// private List FailedProvisions = new List(); /// /// Device constraints for performance profiles, etc /// private Dictionary Constraints = new Dictionary(); /// /// Directory automation artifacts are saved to /// private string LocalTempDir; /// /// Protected constructor - code should use DevicePool.Instance /// protected DevicePool() { lock (LockObject) { // Create two local devices by default for ease of running a client and server AddLocalDevices(2); InitializeReservationServices(); } SkipInstall = Globals.Params.ParseParams("SkipInstall", "SkipCopy", "SkipDeploy"); FullClean = Globals.Params.ParseParam("FullClean"); DeviceReservationBlock = false; } #region IDisposable Support bool bDisposed = false; ~DevicePool() { Dispose(false); } /// /// Shutdown the pool and release all devices. /// public static void Shutdown() { Instance?.Dispose(); Instance = null; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Perform actual dispose behavior /// /// protected virtual void Dispose(bool disposing) { if (bDisposed) { return; } lock (LockObject) { if (disposing) { // Dispose all local devices foreach (ITargetDevice LocalDevice in LocalDevices) { DeviceConfigurationCache.Instance.RevertDeviceConfiguration(LocalDevice); LocalDevice.Dispose(); } // Dispose of any services. These dispose their associated devices foreach (IDeviceReservationService Service in ReservationServices) { Service.Dispose(); } // Cleanup things like platform sdk daemons CleanupDevices(UsedPlatforms); AvailableDevices.Clear(); ClaimedDevices.Clear(); ReservationServices.Clear(); UnprovisionedDevices.Clear(); } } bDisposed = true; } #endregion /// /// Returns the number of available devices of the provided type. This includes unprovisioned devices but not reserved ones. /// Note: unprovisioned devices are currently only returned when device is not constrained /// public int GetAvailableDeviceCount(UnrealDeviceTargetConstraint Constraint, Func Validate = null) { lock (LockObject) { return AvailableDevices.Where(D => Validate == null ? Constraint.Check(D) : Validate(D)).Count() + UnprovisionedDevices.Where(D => Constraint.Check(D)).Count(); } } /// /// Returns the number of available devices of the provided type. This includes unprovisioned devices but not reserved ones /// Note: unprovisioned devices are currently only returned when device is not constrained /// public int GetTotalDeviceCount(UnrealDeviceTargetConstraint Constraint, Func Validate = null) { lock (LockObject) { return AvailableDevices.Union(ClaimedDevices).Where(D => Validate == null ? Constraint.Check(D) : Validate(D)).Count() + UnprovisionedDevices.Where(D => Constraint.Check(D)).Count(); } } public UnrealDeviceTargetConstraint GetConstraint(ITargetDevice Device) { if (!Constraints.ContainsKey(Device)) { throw new AutomationException("Device pool has no contstaint for {0} (device was likely released)", Device); } return Constraints[Device]; } public void SetLocalOptions(string InLocalTemp, bool InUniqueTemps = false, string InDeviceURL = "") { LocalTempDir = InLocalTemp; Legacy_DeviceURL = InDeviceURL; } public void AddLocalDevices(int MaxCount) { AddLocalDevices(MaxCount, BuildHostPlatform.Current.Platform); } public void AddLocalDevices(int MaxCount, UnrealTargetPlatform LocalPlatform) { int NumDevices = GetAvailableDeviceCount(new UnrealDeviceTargetConstraint(LocalPlatform)); for (int i = NumDevices; i < MaxCount; i++) { DeviceDefinition Def = new DeviceDefinition(); Def.Name = string.Format("LocalDevice{0}", i); Def.Platform = LocalPlatform; UnprovisionedDevices.Add(Def); } } public void AddVirtualDevices(int MaxCount) { UnrealTargetPlatform LocalPlat = BuildHostPlatform.Current.Platform; IEnumerable VirtualDevices = Gauntlet.Utils.InterfaceHelpers.FindImplementations() .Where(F => F.CanRunVirtualFromPlatform(LocalPlat)); foreach (IVirtualLocalDevice Device in VirtualDevices) { UnrealTargetPlatform? DevicePlatform = Device.GetPlatform(); if (DevicePlatform != null) { int NumDevices = GetAvailableDeviceCount(new UnrealDeviceTargetConstraint(DevicePlatform)); for (int i = NumDevices; i < MaxCount; i++) { DeviceDefinition Def = new DeviceDefinition(); Def.Name = string.Format("Virtual{0}{1}", DevicePlatform.ToString(), i); Def.Platform = DevicePlatform ?? BuildHostPlatform.Current.Platform; UnprovisionedDevices.Add(Def); } } } } /// /// Created a list of device definitions from the passed in reference. Needs work.... /// /// /// /// public void AddDevices(UnrealTargetPlatform DefaultPlatform, string InputReference, bool ObeyConstraints = true) { lock (LockObject) { List NewDevices = new List(); int SlashIndex = InputReference.IndexOf("\\") >= 0 ? InputReference.IndexOf("\\") : InputReference.IndexOf("/"); bool PossibleFileName = InputReference.IndexOfAny(Path.GetInvalidPathChars()) < 0 && (InputReference.IndexOf(":") == -1 || (InputReference.IndexOf(":") == SlashIndex - 1)); // Did they specify a file? if (PossibleFileName && File.Exists(InputReference)) { Log.Info("Adding devices from {Reference}", InputReference); List DeviceDefinitions = JsonSerializer.Deserialize>( File.ReadAllText(InputReference), new JsonSerializerOptions { PropertyNameCaseInsensitive = true } ); foreach (DeviceDefinition Def in DeviceDefinitions) { Log.Info("Adding {DeviceDetails}", Def); // use Legacy field if it exists if (Def.Platform == null) { Def.Platform = Def.Type; } // check for an availability constraint if (string.IsNullOrEmpty(Def.Available) == false && ObeyConstraints) { // check whether disabled if (String.Compare(Def.Available, "disabled", true) == 0) { Log.Info("Skipping {DeviceName} due to being disabled", Def.Name); continue; } // availability is specified as a range, e.g 21:00-09:00. Match M = Regex.Match(Def.Available, @"(\d{1,2}:\d\d)\s*-\s*(\d{1,2}:\d\d)"); if (M.Success) { DateTime From, To; if (DateTime.TryParse(M.Groups[1].Value, out From) && DateTime.TryParse(M.Groups[2].Value, out To)) { // these are just times so when parsed will have todays date. If the To time is less than // From (22:00-06:00) time it spans midnight so move it to the next day if (To < From) { To = To.AddDays(1); } // if From is in the future (e.g. it's 01:00 and range is 22:00-08:00) we may be in the previous days window, // so move them both back a day if (From > DateTime.Now) { From = From.AddDays(-1); To = To.AddDays(-1); } if (DateTime.Now < From || DateTime.Now > To) { Log.Info("Skipping {DeviceName} due to availability constraint {Constraint}", Def.Name, Def.Available); continue; } } else { Log.Warning("Failed to parse availability {Constraint} for {DeviceName}", Def.Available, Def.Name); } } } Def.RemoveOnShutdown = true; if (Def.Platform == null) { Def.Platform = DefaultPlatform; } UnprovisionedDevices.Add(Def); } // randomize devices so if there's a bad device st the start so we don't always hit it (or we do if its later) UnprovisionedDevices = UnprovisionedDevices.OrderBy(D => Guid.NewGuid()).ToList(); } else { if (string.IsNullOrEmpty(InputReference) == false) { string[] DevicesList = InputReference.Split(','); foreach (string DeviceRef in DevicesList) { // check for :
:|. We pass address:port to device constructor Match M = Regex.Match(DeviceRef, @"(.+?):(.+)"); UnrealTargetPlatform DevicePlatform = DefaultPlatform; string DeviceAddress = DeviceRef; string Model = string.Empty; // When using device services, skip adding non-desktop local devices to pool if any of the services can support that platform bool IsDesktop = DevicePlatform.IsInGroup(UnrealPlatformGroup.Desktop); bool ReservationsEnabled = ReservationServices.Count > 0; bool ServicesCanSupportThisPlatform = ReservationsEnabled && ReservationServices.Where(Service => Service.CanSupportDeviceConstraint(new UnrealDeviceTargetConstraint(DevicePlatform))).Any(); bool DeviceServiceEnabled = ServicesCanSupportThisPlatform; if (!IsDesktop && DeviceRef.Equals("default", StringComparison.OrdinalIgnoreCase) && DeviceServiceEnabled) { continue; } if (M.Success) { if (!UnrealTargetPlatform.TryParse(M.Groups[1].ToString(), out DevicePlatform)) { throw new AutomationException("platform {0} is not a recognized device type", M.Groups[1].ToString()); } DeviceAddress = M.Groups[2].ToString(); // parse device model if (DeviceAddress.Contains("|")) { string[] Components = DeviceAddress.Split(new char[] { '|' }); DeviceAddress = Components[0]; Model = Components[1]; } } Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Added device {Platform}:{Address} to pool", DevicePlatform, DeviceAddress); DeviceDefinition Def = new DeviceDefinition(); Def.Address = DeviceAddress; Def.Name = DeviceAddress; Def.Platform = DevicePlatform; Def.Model = Model; UnprovisionedDevices.Add(Def); } } } } } /// /// Adds the list of devices to our internal availability list /// /// public void RegisterDevices(IEnumerable InDevices) { lock (LockObject) { AvailableDevices = AvailableDevices.Union(InDevices).ToList(); } } /// /// Registers the provided device for availability /// /// /// public void RegisterDevice(ITargetDevice Device, UnrealDeviceTargetConstraint Constraint = null) { lock (LockObject) { if (AvailableDevices.Contains(Device)) { throw new Exception("Device already registered!"); } Constraints[Device] = Constraint ?? new UnrealDeviceTargetConstraint(Device.Platform.Value); UsedPlatforms.Add(Device.Platform); AvailableDevices.Add(Device); if (Log.IsVerbose) { Device.RunOptions = Device.RunOptions & ~CommandUtils.ERunOptions.NoLoggingOfRunCommand; } } } /// /// Run the provided function across all our devices until it returns false. Devices are provisioned on demand (e.g turned from info into an ITargetDevice) /// public void EnumerateDevices(UnrealTargetPlatform Platform, Func Predicate) { EnumerateDevices(new UnrealDeviceTargetConstraint(Platform), Predicate); } public void EnumerateDevices(UnrealDeviceTargetConstraint Constraint, Func Predicate) { lock (LockObject) { List Selection = new List(); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, $"Enumerating devices for constraint {Constraint}"); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, $" Available devices:"); AvailableDevices.ForEach(D => Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, $" {D.Platform}:{D.Name}")); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, $" Unprovisioned devices:"); UnprovisionedDevices.ForEach(D => Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, $" {D}")); // randomize the order of all devices that are of this platform var MatchingProvisionedDevices = AvailableDevices.Where(D => Constraint.Check(D)).ToList(); var MatchingUnprovisionedDevices = UnprovisionedDevices.Where(D => Constraint.Check(D)).ToList(); bool OutOfDevices = false; bool ContinuePredicate = true; do { // Go through all our provisioned devices to see if these fulfill the predicates // requirements ITargetDevice NextDevice = MatchingProvisionedDevices.FirstOrDefault(); while (NextDevice != null && ContinuePredicate) { Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, "Checking {DeviceName} against predicate", NextDevice.Name); MatchingProvisionedDevices.Remove(NextDevice); ContinuePredicate = Predicate(NextDevice); NextDevice = MatchingProvisionedDevices.FirstOrDefault(); } if (ContinuePredicate) { // add more devices if possible OutOfDevices = MatchingUnprovisionedDevices.Count() == 0; DeviceDefinition NextDeviceDef = MatchingUnprovisionedDevices.FirstOrDefault(); if (NextDeviceDef != null) { Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, "Provisioning device {DeviceName} for the pool", NextDeviceDef.Name); // try to create a device. This can fail, but if so we'll just end up back here // on the next iteration ITargetDevice NewDevice = CreateAndRegisterDeviceFromDefinition(NextDeviceDef, Constraint); MatchingUnprovisionedDevices.Remove(NextDeviceDef); UnprovisionedDevices.Remove(NextDeviceDef); if (NewDevice != null) { MatchingProvisionedDevices.Add(NewDevice); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, "Added device {DeviceName} to pool", NewDevice.Name); } else { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to provision {DeviceName}", NextDeviceDef.Name); // track this if (FailedProvisions.Contains(NextDeviceDef) == false) { FailedProvisions.Add(NextDeviceDef); } } } else { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Pool ran out of devices of type {Constraint}!", Constraint); OutOfDevices = true; } } } while (OutOfDevices == false && ContinuePredicate); } } /// /// Claim all devices in the provided list. Once reserved a device will not be seen by any code that /// calls EnumerateDevices /// /// /// public bool ClaimDevices(IEnumerable DeviceList) { lock (LockObject) { // can reserve if not reserved... if (ClaimedDevices.Intersect(DeviceList).Count() > 0) { return false; } // remove these devices from the available list AvailableDevices = AvailableDevices.Where(D => DeviceList.Contains(D) == false).ToList(); ClaimedDevices.AddRange(DeviceList); DeviceList.All(D => UsedPlatforms.Add(D.Platform)); } return true; } public bool ReserveDevicesFromService(Dictionary DeviceTypes) { // Flatten the required devices into an enumerable List RequiredDeviceConstraints = DeviceTypes .SelectMany(KVP => Enumerable.Repeat(KVP.Key, KVP.Value)) .ToList(); // Project each constraint into a supported service Dictionary> ServiceMapping = new(); foreach (UnrealDeviceTargetConstraint Constraint in RequiredDeviceConstraints) { foreach (IDeviceReservationService Service in ReservationServices) { if (Service.CanSupportDeviceConstraint(Constraint)) { if (ServiceMapping.ContainsKey(Service)) { ServiceMapping[Service].Add(Constraint); } else { ServiceMapping.Add(Service, new List { Constraint }); } } } } // Determine if any constraints do not have a service that can supply an appropriate device List DevicesWithService = ServiceMapping.Values.SelectMany(Constraint => Constraint).ToList(); List DevicesLackingService = RequiredDeviceConstraints.Except(DevicesWithService).ToList(); if (DevicesLackingService.Any()) { string Message = "No enabled reservation services were capable of supporting the following constraints:\n"; Message += string.Join("\n\t", DevicesLackingService); Message += "\nThe following reservation services were available:\n"; Message += string.Join("\n\t", ReservationServices.Select(Service => Service.GetType().Name)); Message += "\nEnsure your environment is configured to enable any necessary services."; throw new AutomationException(Message); } foreach (var ReservationPair in ServiceMapping) { if (!ReservationPair.Key.ReserveDevicesFromService(ReservationPair.Value)) { Log.Info("Failed to reserve all devices from service. See above log for details"); return false; } } return true; } /// /// Report target device issue to service with given error message /// public void ReportDeviceError(ITargetDevice Device, string ErrorMessage) { if (TryGetDevicesReservationService(Device, out IDeviceReservationService Service)) { Service.ReportDeviceError(Device.Name, ErrorMessage); } else { Log.Verbose("{Device} was not reserved by a service! Ignoring error report."); } } /// /// Release all devices in the provided list from our reserved list /// /// public void ReleaseDevices(IEnumerable DeviceList) { if (DeviceList == null || !DeviceList.Any()) { return; } lock (LockObject) { List KeepList = new List(); foreach (ITargetDevice Device in DeviceList) { if (LocalDevices.Contains(Device)) { DeviceConfigurationCache.Instance.RevertDeviceConfiguration(Device); KeepList.Add(Device); } else if (TryGetDevicesReservationService(Device, out IDeviceReservationService Service)) { Service.ReleaseDevices([Device]); } else { Log.Info("Attempted to release an unregistered device. Ensure the device is registered with DevicePool.Instance.RegisterDevice first."); } } // Remove any provisioned devices AvailableDevices = AvailableDevices.Where(Device => !DeviceList.Contains(Device)).ToList(); ClaimedDevices = ClaimedDevices.Where(Device => !DeviceList.Contains(Device)).ToList(); // But keep local devices available KeepList.ForEach(D => { AvailableDevices.Remove(D); AvailableDevices.Insert(0, D); }); } } /// /// Checks whether device pool can accommodate requirements, optionally add service devices to meet demand /// public bool CheckAvailableDevices(Dictionary RequiredDevices, IReadOnlyCollection ProblemDevices = null, bool UseServiceDevices = true) { Dictionary AvailableDeviceTypes = new Dictionary(); Dictionary TotalDeviceTypes = new Dictionary(); // Do these "how many available" checks every time because the DevicePool provisions on demand so while it may think it has N machines, // some of them may fail to be provisioned and we could end up with none! // See how many of these types are in device pool (mostly to supply informative info if we can't meet these) foreach (var PlatformRequirement in RequiredDevices) { UnrealDeviceTargetConstraint Constraint = PlatformRequirement.Key; Func Validate = (ITargetDevice Device) => { if (!Constraint.Check(Device)) { return false; } if (ProblemDevices == null) { return true; } foreach (ProblemDevice PDevice in ProblemDevices) { if (PDevice.Platform == Device.Platform && PDevice.Name == Device.Name) { return false; } } return true; }; AvailableDeviceTypes[Constraint] = DevicePool.Instance.GetAvailableDeviceCount(Constraint, Validate); TotalDeviceTypes[Constraint] = DevicePool.Instance.GetTotalDeviceCount(Constraint, Validate); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, "{Constraint}: {Platform} devices required. Total:{Total}, Available:{Available}", Constraint, PlatformRequirement.Value, TotalDeviceTypes[PlatformRequirement.Key], AvailableDeviceTypes[PlatformRequirement.Key]); } // get a list of any platforms where we don't have enough var TooFewTotalDevices = RequiredDevices.Where(KP => TotalDeviceTypes[KP.Key] < RequiredDevices[KP.Key]).Select(KP => KP.Key); var TooFewCurrentDevices = RequiredDevices.Where(KP => AvailableDeviceTypes[KP.Key] < RequiredDevices[KP.Key]).Select(KP => KP.Key); var Devices = TooFewTotalDevices.Concat(TooFewCurrentDevices); // Request devices from the service if we need them if (UseServiceDevices && (TooFewTotalDevices.Count() > 0 || TooFewCurrentDevices.Count() > 0)) { Dictionary DeviceCounts = new Dictionary(); Devices.ToList().ForEach(Platform => DeviceCounts[Platform] = RequiredDevices[Platform]); if (!ReserveDevicesFromService(DeviceCounts)) { return false; } } else { // if we can't ever run then throw an exception if (TooFewTotalDevices.Count() > 0) { var MissingDeviceStrings = TooFewTotalDevices.Select(D => string.Format("Not enough devices of type {0} exist for test. ({1} required, {2} available)", D, RequiredDevices[D], AvailableDeviceTypes[D])); Log.Error(KnownLogEvents.Gauntlet_DeviceEvent, string.Join("\n", MissingDeviceStrings)); throw new AutomationException("Not enough devices available"); } // if we can't run now then return false if (TooFewCurrentDevices.Count() > 0) { var MissingDeviceStrings = TooFewCurrentDevices.Select(D => string.Format("Not enough devices of type {0} available for test. ({1} required, {2} available)", D, RequiredDevices[D], AvailableDeviceTypes[D])); Log.Verbose(KnownLogEvents.Gauntlet_DeviceEvent, string.Join("\n", MissingDeviceStrings)); return false; } } return true; } /// /// Created and registered a device from the provided definition /// /// /// public ITargetDevice CreateAndRegisterDeviceFromDefinition(DeviceDefinition Def, UnrealDeviceTargetConstraint Constraint, IDeviceReservationService Service = null, IDeviceFactory Factory = null) { ITargetDevice NewDevice = null; if (Factory == null) { Factory = InterfaceHelpers.FindImplementations() .Where(F => F.CanSupportPlatform(Def.Platform)) .FirstOrDefault(); } if (Factory == null) { throw new AutomationException("No IDeviceFactory implementation that supports {0}", Def.Platform); } try { bool IsDesktop = Def.Platform != null && UnrealBuildTool.Utils.GetPlatformsInClass(UnrealPlatformClass.Desktop).Contains(Def.Platform!.Value); string ClientTempDir = GetCleanCachePath(Def); if (IsDesktop) { NewDevice = Factory.CreateDevice(Def.Name, ClientTempDir); } else { NewDevice = Factory.CreateDevice(Def.Address, ClientTempDir, Def.DeviceData); } if (NewDevice == null) { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to create device {DeviceName}. Device could not be connected.", Def.Name); return null; } if (NewDevice.IsAvailable == false) { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Assigned device {DeviceName} reports unavailable. Requesting a forced disconnect", NewDevice.Name); NewDevice.Disconnect(true); if (NewDevice.IsAvailable == false) { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Assigned device {DeviceName} still unavailable. Requesting a reboot", NewDevice.Name); NewDevice.Reboot(); } } // Now validate if the kit meets the necessary requirements string Message = string.Empty; if (!TryValidateDeviceRequirements(NewDevice, ref Message)) { if (!string.IsNullOrEmpty(Message)) { if (Service != null) { Service.ReportDeviceError(Def.Name, Message); } } Log.Info("\nSkipping device."); return null; } lock (LockObject) { if (NewDevice != null) { RegisterDevice(NewDevice, Constraint); } } } catch (Exception Ex) { string WarningMessage = $"Failed to create device {Def.Name}. {Ex.Message}\n{Ex.StackTrace}"; if (Ex is DeviceException) { if (Service != null) { Service.ReportDeviceError(Def.Name, WarningMessage); } } else { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, WarningMessage); } } return NewDevice; } private void InitializeReservationServices() { // In order to avoid constructing an instance of the service to determine if it's enabled on any number of types we have to do a bit of generics reflection... foreach (Type ServiceType in InterfaceHelpers.FindTypes(true, true)) { MethodInfo Function = typeof(DevicePool).GetMethod("IsServiceEnabled", BindingFlags.Instance | BindingFlags.NonPublic); MethodInfo Generic = Function.MakeGenericMethod(ServiceType); object ReturnValue = Generic.Invoke(this, null); bool IsEnabled = (bool)ReturnValue; if (IsEnabled) { ReservationServices.Add(Activator.CreateInstance(ServiceType) as IDeviceReservationService); } } } private bool IsServiceEnabled() where T : IDeviceReservationService { return T.Enabled; } /// /// Verifies if the provided TargetDevice meets requirements such as firmware, login status, settings, etc. /// /// The device to validate /// The output message when failing to validate /// True if the device matches the required specifications protected bool TryValidateDeviceRequirements(ITargetDevice Device, ref string Message) { IEnumerable Validators = InterfaceHelpers.FindImplementations(true).Where(Validator => Validator.bEnabled); if (!Validators.Any()) { return true; } bool bInitiallyConnected = Device.IsConnected; Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "\nValidating requirements for {DeviceName}...", Device.Name); bool bSucceeded = true; List MessageAggregate = new(); foreach (IDeviceValidator Validator in Validators) { Log.Info("\nStarting validation for {Validator}", Validator); string ValidationMessage = string.Empty; if(!Validator.TryValidateDevice(Device, ref ValidationMessage)) { Log.Info("Failed!"); bSucceeded = false; if (!string.IsNullOrEmpty(ValidationMessage)) { MessageAggregate.Add(ValidationMessage); } } else { Log.Info("Success!"); } } if(!bSucceeded) { Message = string.Join("\n", MessageAggregate.Where(M => !string.IsNullOrEmpty(M))); Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "\nFailed to validate requirements on device {DeviceName}", Device.Name); } else { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "\nAll validators passed, selecting device {DeviceName}\n", Device.Name); } // Most validators require establishing a connection to the device. // If we weren't originally connected, disconnect so the initial connection state can be cached during device reservation if(!bInitiallyConnected && Device.IsConnected) { Device.Disconnect(); } return bSucceeded; } /// /// Explicitly release all device reservations /// private void ReleaseReservations() { foreach (IDeviceReservationService ReservationService in ReservationServices) { foreach (ITargetDevice Device in ReservationService.ReservedDevices) { AvailableDevices.Remove(Device); ClaimedDevices.Remove(Device); } ReservationService.ReleaseDevices(ReservationService.ReservedDevices); } } private void CleanupDevices(IEnumerable Platforms) { IEnumerable DeviceServices = InterfaceHelpers.FindImplementations(); if (DeviceServices.Any()) { foreach (UnrealTargetPlatform? Platform in Platforms) { IDeviceService DeviceService = DeviceServices.Where(D => D.CanSupportPlatform(Platform)).FirstOrDefault(); if (DeviceService != null) { DeviceService.CleanupDevices(); } } } } private bool TryGetDevicesReservationService(ITargetDevice Device, out IDeviceReservationService OutService) { OutService = null; foreach (IDeviceReservationService Service in ReservationServices) { if (Service.ReservedDevices.Contains(Device)) { OutService = Service; return true; } } return false; } /// /// Construct a path to hold cache files and make sure it's properly cleaned /// private string GetCleanCachePath(DeviceDefinition InDeviceDefiniton) { // Give the desktop platform a temp folder with its name under the device cache string DeviceCache = Path.Combine(LocalTempDir, "DeviceCache"); string PlatformCache = Path.Combine(DeviceCache, InDeviceDefiniton.Platform.ToString()); string ClientCache = Path.Combine(PlatformCache, InDeviceDefiniton.Name); // On Desktops, builds are installed in the device cache. // When using device reservation blocks, we don't want to fully clean the cache and lose previously installed builds. // If bRetainBuilds evaluates to true, it means we are in the second step or beyond in a device reservation block. // In this case we'll just delete the left over UserDir which should already have been emptied by UnrealSession. bool bRetainCache = Globals.Params.ParseParam("RetainCache"); bool bRetainBuilds = SkipInstall && !FullClean; if(bRetainBuilds || bRetainCache) { Log.Info("Retaining build cache for device reservation block"); DirectoryInfo UserDirectory = new(Path.Combine(ClientCache, "UserDir")); if(UserDirectory.Exists) { try { Log.Info("Cleaning stale user directory..."); SystemHelpers.Delete(UserDirectory, true, true); } catch(Exception Ex) { throw new AutomationException("Failed to clean user directory {0}. This could result in improper artifact reporting. {1}", UserDirectory, Ex); } } } else { int CleanAttempts = 0; while (Directory.Exists(ClientCache)) { DirectoryInfo ClientCacheDirectory = new(ClientCache); try { Log.Info("Cleaning stale client device cache..."); SystemHelpers.Delete(ClientCacheDirectory, true, true); } catch (Exception Ex) { // If we fail to acquire the default client cache while using device reservation blocks, // we can't ensure future tests will have their cache directories mapped to the correct build location if (DeviceReservationBlock) { throw new AutomationException("Failed to clean default client device cache {0}. {1}", ClientCache, Ex); } // When not using device reservation blocks, we can just create a newly indexed directory for the client cache else { string Warning = "Failed to clean client device cache {Folder}. A newly indexed directory will be created instead. {Message}"; Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, Warning, ClientCache, Ex.Message); ClientCache = Path.Combine(PlatformCache, $"{InDeviceDefiniton.Name}_{++CleanAttempts}"); } } } // create this path Log.Info("Client device cache set to {Directory}", ClientCache); Directory.CreateDirectory(ClientCache); } return ClientCache; } #region Legacy Implementation [Obsolete("Will be removed in a future release")] public string DeviceURL { get { return Legacy_DeviceURL; } set { Legacy_DeviceURL = value; } } /// /// Device reservation service URL /// private string Legacy_DeviceURL; [Obsolete("Misnomer, use ClaimDevices instead. Will be removed in a future release")] public void ReserveDevices(IEnumerable Devices) { ClaimDevices(Devices); } [Obsolete("Will be removed in a future release. Use the single parameter overload instead.")] public bool ReserveDevicesFromService(string DeviceURL, Dictionary DeviceTypes) { return ReserveDevicesFromService(DeviceTypes); } #endregion } }