/// <summary> /// Checks whether version of deployed bundle matches local IPA /// </summary> bool CheckDeployedIPA(IOSBuild Build) { try { Log.Verbose("Checking deployed IPA hash"); string CommandLine = String.Format("--bundle_id {0} --download={1} --to {2}", Build.PackageName, "/Documents/IPAHash.txt", LocalCachePath); IProcessResult Result = ExecuteIOSDeployCommand(CommandLine, 120); if (Result.ExitCode != 0) { return(false); } string Hash = File.ReadAllText(LocalCachePath + "/Documents/IPAHash.txt").Trim(); string StoredHash = File.ReadAllText(IPAHashFilename).Trim(); if (Hash == StoredHash) { Log.Verbose("Deployed app hash matched cached IPA hash"); return(true); } } catch (Exception Ex) { if (!Ex.Message.Contains("is denied")) { Log.Verbose("Unable to pull cached IPA cache from device, cached file may not exist: {0}", Ex.Message); } } Log.Verbose("Deployed app hash doesn't match, IPA will be installed"); return(false); }
// Get rid of any zombie lldb/iosdeploy processes, this needs to be reworked to use tracked process id's when running parallel tests across multiple AutomationTool.exe processes on test workers void KillZombies() { if (Globals.IsWorker || ZombiesKilled) { return; } ZombiesKilled = true; IOSBuild.ExecuteCommand("killall", "ios-deploy"); Thread.Sleep(2500); IOSBuild.ExecuteCommand("killall", "lldb"); Thread.Sleep(2500); }
public static IEnumerable <IOSBuild> CreateFromPath(string InProjectName, string InPath) { string BuildPath = InPath; List <IOSBuild> DiscoveredBuilds = new List <IOSBuild>(); DirectoryInfo Di = new DirectoryInfo(BuildPath); // find all install batchfiles FileInfo[] InstallFiles = Di.GetFiles("*.ipa"); foreach (FileInfo Fi in InstallFiles) { var UnrealConfig = UnrealHelpers.GetConfigurationFromExecutableName(InProjectName, Fi.Name); Log.Verbose("Pulling package data from {0}", Fi.FullName); string AbsPath = Fi.Directory.FullName; // IOS builds are always packaged, and can always replace the command line and executable as we cache the unzip'd IPA BuildFlags Flags = BuildFlags.Packaged | BuildFlags.CanReplaceCommandLine | BuildFlags.CanReplaceExecutable; if (AbsPath.Contains("Bulk")) { Flags |= BuildFlags.Bulk; } else { Flags |= BuildFlags.NotBulk; } string SourceIPAPath = Fi.FullName; string PackageName = GetBundleIdentifier(SourceIPAPath); if (String.IsNullOrEmpty(PackageName)) { continue; } Dictionary <string, string> FilesToInstall = new Dictionary <string, string>(); IOSBuild NewBuild = new IOSBuild(UnrealConfig, PackageName, SourceIPAPath, FilesToInstall, Flags); DiscoveredBuilds.Add(NewBuild); Log.Verbose("Found {0} {1} build at {2}", UnrealConfig, ((Flags & BuildFlags.Bulk) == BuildFlags.Bulk) ? "(bulk)" : "(not bulk)", AbsPath); } return(DiscoveredBuilds); }
public bool Reboot() { const string Cmd = "/usr/local/bin/idevicediagnostics"; if (!File.Exists(Cmd)) { Log.Verbose("Rebooting iOS device requires idevicediagnostics binary"); return(true); } var Result = IOSBuild.ExecuteCommand(Cmd, string.Format("restart -u {0}", DeviceName)); if (Result.ExitCode != 0) { Log.Warning(string.Format("Failed to reboot iOS device {0}, restart command failed", DeviceName)); return(true); } // initial wait 20 seconds Thread.Sleep(20 * 1000); const int WaitPeriod = 10; int WaitTime = 120; bool rebooted = false; do { Result = IOSBuild.ExecuteCommand(Cmd, string.Format("diagnostics WiFi -u {0}", DeviceName)); if (Result.ExitCode == 0) { rebooted = true; break; } Thread.Sleep(WaitPeriod * 1000); WaitTime -= WaitPeriod; } while (WaitTime > 0); if (!rebooted) { Log.Warning("Failed to reboot iOS device {0}, device didn't come back after restart", DeviceName); } return(true); }
/// <summary> /// Remove artifacts from device /// </summary> private bool CleanDeviceArtifacts(IOSBuild Build) { try { Log.Verbose("Cleaning device artifacts"); string CleanCommand = String.Format("--bundle_id {0} --rm_r {1}", Build.PackageName, DeviceArtifactPath); IProcessResult Result = ExecuteIOSDeployCommand(CleanCommand, 120); if (Result.ExitCode != 0) { Log.Warning("Failed to clean artifacts from device"); return(false); } } catch (Exception Ex) { Log.Verbose("Exception while cleaning artifacts from device: {0}", Ex.Message); } return(true); }
/// <summary> /// Generate MD5 and cache IPA bundle files /// </summary> private bool PrepareIPA(IOSBuild Build) { Log.Info("Preparing IPA {0}", Build.SourceIPAPath); try { // cache the unzipped app using a MD5 checksum, avoiding needing to unzip string Hash = null; string StoredHash = null; using (var MD5Hash = MD5.Create()) { using (var Stream = File.OpenRead(Build.SourceIPAPath)) { Hash = BitConverter.ToString(MD5Hash.ComputeHash(Stream)).Replace("-", "").ToLowerInvariant(); } } string PayloadDir = Path.Combine(GauntletAppCache, "Payload"); string SymbolsDir = Path.Combine(GauntletAppCache, "Symbols"); if (File.Exists(IPAHashFilename) && Directory.Exists(PayloadDir)) { StoredHash = File.ReadAllText(IPAHashFilename).Trim(); if (Hash != StoredHash) { Log.Verbose("IPA hash out of date, clearing cache"); StoredHash = null; } } if (String.IsNullOrEmpty(StoredHash) || Hash != StoredHash) { if (Directory.Exists(PayloadDir)) { Directory.Delete(PayloadDir, true); } if (Directory.Exists(SymbolsDir)) { Directory.Delete(SymbolsDir, true); } if (File.Exists(CacheResignedFilename)) { File.Delete(CacheResignedFilename); } Log.Verbose("Unzipping IPA {0} to cache at: {1}", Build.SourceIPAPath, GauntletAppCache); string Output; if (!IOSBuild.ExecuteIPAZipCommand(String.Format("{0} -d {1}", Build.SourceIPAPath, GauntletAppCache), out Output, PayloadDir)) { throw new Exception(String.Format("Unable to extract IPA {0}", Build.SourceIPAPath)); } // Cache symbols for symbolicated callstacks string SymbolsZipFile = string.Format("{0}/../../Symbols/{1}.dSYM.zip", Path.GetDirectoryName(Build.SourceIPAPath), Path.GetFileNameWithoutExtension(Build.SourceIPAPath)); Log.Verbose("Checking Symbols at {0}", SymbolsZipFile); if (File.Exists(SymbolsZipFile)) { Log.Verbose("Unzipping Symbols {0} to cache at: {1}", SymbolsZipFile, SymbolsDir); if (!IOSBuild.ExecuteIPAZipCommand(String.Format("{0} -d {1}", SymbolsZipFile, SymbolsDir), out Output, SymbolsDir)) { throw new Exception(String.Format("Unable to extract build symbols {0} -> {1}", SymbolsZipFile, SymbolsDir)); } } // store hash File.WriteAllText(IPAHashFilename, Hash); Log.Verbose("IPA cached"); } else { Log.Verbose("Using cached IPA"); } LocalAppBundle = Directory.GetDirectories(PayloadDir).Where(D => Path.GetExtension(D) == ".app").FirstOrDefault(); if (String.IsNullOrEmpty(LocalAppBundle)) { throw new Exception(String.Format("Unable to find app in local app bundle {0}", PayloadDir)); } } catch (Exception Ex) { throw new AutomationException("Unable to prepare {0} : {1}", Build.SourceIPAPath, Ex.Message); } return(true); }
public IAppInstall InstallApplication(UnrealAppConfig AppConfig) { IOSBuild Build = AppConfig.Build as IOSBuild; // Ensure Build exists if (Build == null) { throw new AutomationException("Invalid build for IOS!"); } bool CacheResigned = false; bool UseLocalExecutable = Globals.Params.ParseParam("dev"); lock (IPALock) { Log.Info("Installing using IPA {0}", Build.SourceIPAPath); // device artifact path DeviceArtifactPath = string.Format("/Documents/{0}/Saved", AppConfig.ProjectName); CacheResigned = File.Exists(CacheResignedFilename); if (CacheResigned && !UseLocalExecutable) { if (File.Exists(IPAHashFilename)) { Log.Verbose("App was resigned, invalidating app cache"); File.Delete(IPAHashFilename); } } PrepareIPA(Build); // local executable support if (UseLocalExecutable) { ResignApplication(AppConfig); } } if (CacheResigned || UseLocalExecutable || !CheckDeployedIPA(Build)) { // uninstall will clean all device artifacts ExecuteIOSDeployCommand(String.Format("--uninstall -b \"{0}\"", LocalAppBundle), 10 * 60); } else { // remove device artifacts CleanDeviceArtifacts(Build); } // parallel iOS tests use same app install folder, so lock it as setup is quick lock (Globals.MainLock) { // local app install with additional files, this directory will be mirrored to device in a single operation string AppInstallPath; AppInstallPath = Path.Combine(Globals.TempDir, "iOSAppInstall"); if (Directory.Exists(AppInstallPath)) { Directory.Delete(AppInstallPath, true); } Directory.CreateDirectory(AppInstallPath); if (LocalDirectoryMappings.Count == 0) { PopulateDirectoryMappings(AppInstallPath); } //@todo: Combine Build and AppConfig files, this should be done in higher level code, not per device implementation if (AppConfig.FilesToCopy != null) { foreach (UnrealFileToCopy FileToCopy in AppConfig.FilesToCopy) { string PathToCopyTo = Path.Combine(LocalDirectoryMappings[FileToCopy.TargetBaseDirectory], FileToCopy.TargetRelativeLocation); if (File.Exists(FileToCopy.SourceFileLocation)) { FileInfo SrcInfo = new FileInfo(FileToCopy.SourceFileLocation); SrcInfo.IsReadOnly = false; string DirectoryToCopyTo = Path.GetDirectoryName(PathToCopyTo); if (!Directory.Exists(DirectoryToCopyTo)) { Directory.CreateDirectory(DirectoryToCopyTo); } if (File.Exists(PathToCopyTo)) { FileInfo ExistingFile = new FileInfo(PathToCopyTo); ExistingFile.IsReadOnly = false; } SrcInfo.CopyTo(PathToCopyTo, true); Log.Verbose("Copying app install: {0} to {1}", FileToCopy, DirectoryToCopyTo); } else { Log.Warning("File to copy {0} not found", FileToCopy); } } } // copy mapped files in a single pass string CopyCommand = String.Format("--bundle_id {0} --upload={1} --to {2}", Build.PackageName, AppInstallPath, DeviceArtifactPath); ExecuteIOSDeployCommand(CopyCommand, 120); // store the IPA hash to avoid redundant deployments CopyCommand = String.Format("--bundle_id {0} --upload={1} --to {2}", Build.PackageName, IPAHashFilename, "/Documents/IPAHash.txt"); ExecuteIOSDeployCommand(CopyCommand, 120); } IOSAppInstall IOSApp = new IOSAppInstall(AppConfig.Name, this, Build.PackageName, AppConfig.CommandLine); return(IOSApp); }
/// <summary> /// Resign application using local executable and update debug symbols /// </summary> void ResignApplication(UnrealAppConfig AppConfig) { // check that we have the signing stuff we need string SignProvision = Globals.Params.ParseValue("signprovision", String.Empty); string SignEntitlements = Globals.Params.ParseValue("signentitlements", String.Empty); string SigningIdentity = Globals.Params.ParseValue("signidentity", String.Empty); // handle signing provision if (string.IsNullOrEmpty(SignProvision) || !File.Exists(SignProvision)) { throw new AutomationException("Absolute path to existing provision must be specified, example: -signprovision=/path/to/myapp.provision"); } // handle entitlements // Note this extracts entitlements: which may be useful when using same provision/entitlements?: codesign -d --entitlements :entitlements.plist ~/.gauntletappcache/Payload/Example.app/ if (string.IsNullOrEmpty(SignEntitlements) || !File.Exists(SignEntitlements)) { throw new AutomationException("Absolute path to existing entitlements must be specified, example: -signprovision=/path/to/entitlements.plist"); } // signing identity if (string.IsNullOrEmpty(SigningIdentity)) { throw new AutomationException("Signing identity must be specified, example: -signidentity=\"iPhone Developer: John Smith\""); } string ProjectName = AppConfig.ProjectName; string BundleName = Path.GetFileNameWithoutExtension(LocalAppBundle); string ExecutableName = UnrealHelpers.GetExecutableName(ProjectName, UnrealTargetPlatform.IOS, AppConfig.Configuration, AppConfig.ProcessType, ""); string CachedAppPath = Path.Combine(GauntletAppCache, "Payload", string.Format("{0}.app", BundleName)); string LocalExecutable = Path.Combine(Environment.CurrentDirectory, ProjectName, string.Format("Binaries/IOS/{0}", ExecutableName)); if (!File.Exists(LocalExecutable)) { throw new AutomationException("Local executable not found for -dev argument: {0}", LocalExecutable); } File.WriteAllText(CacheResignedFilename, "The application has been resigned"); // copy local executable FileInfo SrcInfo = new FileInfo(LocalExecutable); string DestPath = Path.Combine(CachedAppPath, BundleName); SrcInfo.CopyTo(DestPath, true); Log.Verbose("Copied local executable from {0} to {1}", LocalExecutable, DestPath); // copy provision SrcInfo = new FileInfo(SignProvision); DestPath = Path.Combine(CachedAppPath, "embedded.mobileprovision"); SrcInfo.CopyTo(DestPath, true); Log.Verbose("Copied provision from {0} to {1}", SignProvision, DestPath); // handle symbols string LocalSymbolsDir = Path.Combine(Environment.CurrentDirectory, ProjectName, string.Format("Binaries/IOS/{0}.dSYM", ExecutableName)); DestPath = Path.Combine(GauntletAppCache, string.Format("Symbols/{0}.dSYM", ExecutableName)); if (Directory.Exists(DestPath)) { Directory.Delete(DestPath, true); } if (Directory.Exists(LocalSymbolsDir)) { CommandUtils.CopyDirectory_NoExceptions(LocalSymbolsDir, DestPath, true); } else { Log.Warning("No symbols found for local build at {0}, removing cached app symbols", LocalSymbolsDir); } // resign application // @todo: this asks for password unless "Always Allow" is selected, also for builders, document how to permanently grant codesign access to keychain string SignArgs = string.Format("-f -s \"{0}\" --entitlements \"{1}\" \"{2}\"", SigningIdentity, SignEntitlements, CachedAppPath); Log.Info("\nResigning app, please enter keychain password if prompted:\n\ncodesign {0}", SignArgs); var Result = IOSBuild.ExecuteCommand("codesign", SignArgs); if (Result.ExitCode != 0) { throw new AutomationException("Failed to resign application"); } }
/// <summary> /// Remove the application entirely from the iOS device, this includes any persistent app data in /Documents /// </summary> private void RemoveApplication(IOSBuild Build) { string CommandLine = String.Format("--bundle_id {0} --uninstall_only", Build.PackageName); ExecuteIOSDeployCommand(CommandLine); }
public List <IBuild> GetBuildsAtPath(string InProjectName, string InPath, int MaxRecursion = 3) { // We only want iOS builds on Mac host if (BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { return(new List <IBuild>()); } List <DirectoryInfo> AllDirs = new List <DirectoryInfo>(); List <IBuild> Builds = new List <IBuild>(); // c:\path\to\build DirectoryInfo PathDI = new DirectoryInfo(InPath); if (PathDI.Exists) { if (PathDI.Name.IndexOf("IOS", StringComparison.OrdinalIgnoreCase) >= 0) { AllDirs.Add(PathDI); } // find all directories that begin with IOS DirectoryInfo[] IOSDirs = PathDI.GetDirectories("IOS*", SearchOption.TopDirectoryOnly); AllDirs.AddRange(IOSDirs); List <DirectoryInfo> DirsToRecurse = AllDirs; // now get subdirs while (MaxRecursion-- > 0) { List <DirectoryInfo> DiscoveredDirs = new List <DirectoryInfo>(); DirsToRecurse.ToList().ForEach((D) => { DiscoveredDirs.AddRange(D.GetDirectories("*", SearchOption.TopDirectoryOnly)); }); AllDirs.AddRange(DiscoveredDirs); DirsToRecurse = DiscoveredDirs; } //IOSBuildSource BuildSource = null; string IOSBuildFilter = Globals.Params.ParseValue("IOSBuildFilter", ""); foreach (DirectoryInfo Di in AllDirs) { IEnumerable <IOSBuild> FoundBuilds = IOSBuild.CreateFromPath(InProjectName, Di.FullName); if (FoundBuilds != null) { if (!string.IsNullOrEmpty(IOSBuildFilter)) { //IndexOf used because Contains must be case-sensitive FoundBuilds = FoundBuilds.Where(B => B.SourceIPAPath.IndexOf(IOSBuildFilter, StringComparison.OrdinalIgnoreCase) >= 0); } Builds.AddRange(FoundBuilds); } } } return(Builds); }