// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Text.RegularExpressions; using AutomationTool; using Newtonsoft.Json; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.iOS; using OpenQA.Selenium.Appium.Service; using static AutomationTool.CommandUtils; namespace Gauntlet { /// /// AppiumContainer is a wrapper for both an Appium server instance and an AppiumDriver. /// It's primary use in Gauntlet is for automating dismissal of blocking system notifications that cannot be managed by an MDM profile. /// Provided you configure your environment correctly, the container will automatically initialize as part of an IOS app instance execution. /// /// Steps for configuring your environment: /// 1. Download appium on your host. It's recommended you use the global npm installation 'npm install -g appium'. /// 2. For real device testing, you will need a WebDriverAgentRunner.app which is signed with a mobile provision that includes your device. /// For this you have two options: /// - Build the app from source. This lets you configure bundle.id's if your signing cert only allows for certain identifiers. /// - Download the precompiled app and re-sign after replacing the embedded mobileprovision. /// In either case, you can find both the source and precompiled app on this page https://github.com/appium/WebDriverAgent/releases /// 3. Create a JSON file that can be de-serialized to the 'AppiumContainer.Config' type. This file is used to configure the driver with information /// that is specific to your team. Place this file in a location that can be read by your host. /// 4. Point the container to the location of the json file you created in step 4 by doing one of the following: /// - Setting the UE-AppiumConfigPath EnvVar to a qualified path or a relative path to your UE root. /// - Run UAT with -AppiumConfigPath=/path set to a qualified path or a relative path to your UE root. /// /// Once all these steps are completed, before TargetDeviceIOS.Run starts the app process, it will execute these actions: /// 1. Start an appium server on an available loopback port /// 2. Install the WebDriverAgent app /// 3. Start the driver with your configured settings /// /// From there appium will automatically accept/dismiss any system prompts it encounters. /// public class AppiumContainer : IDisposable { class Config { /// /// Name of the automation driver to use. Defaults to XCUITest if not specified /// public string AutomationName { get; set; } = "xcuitest"; /// /// Your company's apple developer team ID /// public string OrgId { get; set; } /// /// Identity of your signing cert. Usually just 'Apple Development' /// public string SigningId { get; set; } /// /// Path to a precompiled WebDriverAgentRunner app /// public string WdaAppPath { get; set; } /// /// Bundle ID of the WebDriverAgent app. Ex: 'com.epicgames.WebDriverAgent' /// public string WdaBundleId { get; set; } /// /// Optional - Allow you to override the location of the appium executable. /// Useful if you opt not to install appium to the global npm root /// public string AppiumLocation { get; set; } public AppiumOptions GetCapabilities(string UUID, string PackageName) { AppiumOptions Capabilities = new AppiumOptions(); Capabilities.PlatformName = "iOS"; Capabilities.AutomationName = AutomationName; Capabilities.AddAdditionalAppiumOption("udid", UUID); Capabilities.AddAdditionalAppiumOption("bundleId", PackageName); Capabilities.AddAdditionalAppiumOption("autoAcceptAlerts", true); Capabilities.AddAdditionalAppiumOption("xcodeOrgId", OrgId); Capabilities.AddAdditionalAppiumOption("xcodeSigningId", SigningId); Capabilities.AddAdditionalAppiumOption("autoLaunch", false); Capabilities.AddAdditionalAppiumOption("usePreinstalledWDA", true); Capabilities.AddAdditionalAppiumOption("updatedWDABundleId", WdaBundleId); if (Log.Level == LogLevel.Verbose) { Capabilities.AddAdditionalAppiumOption("showXcodeLog", true); } return Capabilities; } } private const string AppiumConfigEnvVar = "UE-AppiumConfigPath"; private const string AppiumConfigArg = "AppiumConfigPath"; /// /// **REQUIRED** /// Path to a json file that can be deserialized into the AppiumContainer.Config type /// This allows users to specify things such as the org id, signing identity, etc. /// Can be overriden by setting the UE-AppiumConfigPath envvar, or by running with -AppiumConfigPath=/path /// Path can be relative or absolute /// public static string AppiumConfigPath = "Engine/Restricted/NotForLicensees/Extras/ThirdPartyNotUE/WebDriverAgent/AppiumConfig.json"; /// /// Whether or not the appium container is configured properly for use. /// This requires the appium config path and the appium server path to point to existing files /// public static bool Enabled => AppiumConfig != null; /// /// Lock object /// private static object Mutex = new(); /// /// Config file used for the Driver /// private static Config AppiumConfig = null; /// /// Driver handle /// private IOSDriver Driver = null; /// /// Server handle /// private AppiumLocalService Server = null; /// /// UUID of the device being tests /// private string UUID = null; static AppiumContainer() { if (Globals.Params.ParseParam("NoAppium")) { return; } string ConfigEnvVar = Environment.GetEnvironmentVariable(AppiumConfigEnvVar); if (!string.IsNullOrEmpty(ConfigEnvVar)) { AppiumConfigPath = ConfigEnvVar; } AppiumConfigPath = Globals.Params.ParseValue(AppiumConfigArg, AppiumConfigPath); if (FileExists(AppiumConfigPath)) { try { AppiumConfig = JsonConvert.DeserializeObject(ReadAllText(AppiumConfigPath)); } catch (Exception Ex) { throw new AutomationException(Ex, "Failed to derserialize AppiumConfig at {0}", AppiumConfigPath); } } } public AppiumContainer(string UUID) { this.UUID = UUID; ConfigureDrivers(); } #region IDisposable Support private bool Disposed = false; ~AppiumContainer() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool bDisposing) { if (!Disposed) { if (bDisposing) { } Stop(); Disposed = true; } } #endregion public void Start(string PackageName) { Stop(); try { IProcessResult InstallResult; using (ScopedSuspendECErrorParsing ErrorSuspension = new()) { if (TargetDeviceIOS.UseDeviceCtl) { InstallResult = TargetDeviceIOS.ExecuteDevicectlCommand( string.Format("device install app \"{0}\" -v", AppiumConfig.WdaAppPath), UUID, 60, AdditionalOptions: ERunOptions.NoStdOutRedirect); } else { InstallResult = TargetDeviceIOS.ExecuteIOSDeployCommand( string.Format("-b \"{0}\"", AppiumConfig.WdaAppPath), UUID, 60, AdditionalOptions: ERunOptions.NoStdOutRedirect); } } if (InstallResult.ExitCode != 0) { throw new AutomationException("Failed to install WDA app ({0}): {1}", InstallResult.ExitCode, InstallResult.Output); } lock (Mutex) { AppiumServiceBuilder ServerBuilder = new AppiumServiceBuilder() .UsingAnyFreePort() .WithIPAddress("127.0.0.1"); if (!string.IsNullOrEmpty(AppiumConfig.AppiumLocation) && FileExists(AppiumConfig.AppiumLocation)) { ServerBuilder.WithAppiumJS(new System.IO.FileInfo(AppiumConfig.AppiumLocation)); } Server = ServerBuilder.Build(); Server.Start(); Driver = new IOSDriver(Server, AppiumConfig.GetCapabilities(UUID, PackageName)); } } catch (Exception Ex) { throw new AutomationException(Ex, "Failed to start appium instance for {0}: {1}", UUID, Ex.Message); } } public void Stop() { if (Driver != null) { Driver.Dispose(); Driver = null; } if (Server != null) { Server.Dispose(); Server = null; } // TODO: Terminate WDA app - parse devicectl for pid, then kill? } private void ConfigureDrivers() { // Query installed drivers IProcessResult Result = Run("appium", "driver list"); if (Result.ExitCode != 0) { throw new AutomationException("Failed to query appium driver list ({0}): {1}", Result.ExitCode, Result.Output); } // Trim ansii escape codes string SanitizedOutput = Regex.Replace(Result.Output, "\\x1B\\[[0-9;]*[a-zA-Z]", string.Empty); // Try to find the driver for for this container's config bool bFoundDriver = false; Regex DriverMatch = new Regex("(- )(.*?)(@)(\\d+((\\.\\d+)+)?)", RegexOptions.IgnoreCase | RegexOptions.Multiline); foreach (Match Match in DriverMatch.Matches(SanitizedOutput)) { string Driver = Match.Groups[2].Value; string Version = Match.Groups[4].Value; if (Driver.Equals(AppiumConfig.AutomationName, StringComparison.OrdinalIgnoreCase)) { bFoundDriver = true; Log.Verbose("Using {AppiumDriverName} appium driver version {AppiumDriverVersion}", Driver, Version); break; } } // Install the driver if the missing if (!bFoundDriver) { Log.Info("Could not find appium driver {AppiumDriverName}. Attempting to install...", AppiumConfig.AutomationName); Result = Run("appium", $"driver install {AppiumConfig.AutomationName.ToLower()}"); if (Result.ExitCode != 0) { throw new AutomationException("Failed to install {0} appium driver. ({1}): {2}", AppiumConfig.AutomationName, Result.ExitCode, Result.Output); } } } } }