/// <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);
        }