diff --git a/Appium Wizard/Appium Wizard.csproj b/Appium Wizard/Appium Wizard.csproj index 2e0f21e..82449f6 100644 --- a/Appium Wizard/Appium Wizard.csproj +++ b/Appium Wizard/Appium Wizard.csproj @@ -209,6 +209,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Appium Wizard/Appium Wizard.csproj.user b/Appium Wizard/Appium Wizard.csproj.user index 53c140c..377ee90 100644 --- a/Appium Wizard/Appium Wizard.csproj.user +++ b/Appium Wizard/Appium Wizard.csproj.user @@ -19,6 +19,9 @@ Form + + Form + Form diff --git a/Appium Wizard/ExcludeInInstaller/Installer Script.iss b/Appium Wizard/ExcludeInInstaller/Installer Script.iss index ecb7d18..042459c 100644 --- a/Appium Wizard/ExcludeInInstaller/Installer Script.iss +++ b/Appium Wizard/ExcludeInInstaller/Installer Script.iss @@ -2,12 +2,12 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Appium Wizard" -#define MyAppVersion "8.6.0" +#define MyAppVersion "8.6.1" #define MyAppPublisher "Meganathan C" #define MyAppExeName "Appium Wizard.exe" [Setup] -AlwaysRestart = no +AlwaysRestart = no AppId={{408E2B01-BCF1-40E3-8F27-F11B32572D03} AppName={#MyAppName} UninstallDisplayName = Appium Wizard diff --git a/Appium Wizard/FilesPath.cs b/Appium Wizard/FilesPath.cs index bf570af..3d9d33d 100644 --- a/Appium Wizard/FilesPath.cs +++ b/Appium Wizard/FilesPath.cs @@ -24,5 +24,7 @@ public static class FilesPath public static string FFMpegFilePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + "\\Resources\\Executables\\FFMpeg\\"; public static string logsFilesPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + "\\Resources\\Logs\\"; public static string scrcpy = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + "\\Resources\\Executables\\scrcpy\\scrcpy.exe"; + public static string ddiDownloaderFilePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + "\\Resources\\iOS\\ddi-downloader.exe"; + public static string devImagesPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + "\\Resources\\iOS\\devimages\\"; } } diff --git a/Appium Wizard/MainScreen.cs b/Appium Wizard/MainScreen.cs index a357d3b..b74f461 100644 --- a/Appium Wizard/MainScreen.cs +++ b/Appium Wizard/MainScreen.cs @@ -28,6 +28,7 @@ public partial class MainScreen : Form public static bool useScrcpy; //public static List UDIDPreInstalledWDA = new List(); public static Dictionary UDIDPreInstalledWDA = new Dictionary(); + public static HashSet UDIDsUsingRunWDA = new HashSet(); // Devices that should use runwda command instead of launch private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private Timer uncheckTimer; private Timer highlightTimer; public static Dictionary serverNumberWebView = new Dictionary(); diff --git a/Appium Wizard/Object Spy.Designer.cs b/Appium Wizard/Object Spy.Designer.cs index c839434..6d636d7 100644 --- a/Appium Wizard/Object Spy.Designer.cs +++ b/Appium Wizard/Object Spy.Designer.cs @@ -54,6 +54,7 @@ private void InitializeComponent() addUniqueXpathToFilterToolStripMenuItem1 = new ToolStripMenuItem(); coordLabel = new Label(); helpButton = new Button(); + downloadXmlButton = new Button(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); listViewContextMenuStrip.SuspendLayout(); treeViewContextMenuStrip.SuspendLayout(); @@ -265,6 +266,17 @@ private void InitializeComponent() helpButton.UseVisualStyleBackColor = true; helpButton.Click += helpButton_Click; // + // downloadXmlButton + // + downloadXmlButton.AutoSize = true; + downloadXmlButton.Location = new Point(475, 8); + downloadXmlButton.Name = "downloadXmlButton"; + downloadXmlButton.Size = new Size(98, 25); + downloadXmlButton.TabIndex = 16; + downloadXmlButton.Text = "Download XML"; + downloadXmlButton.UseVisualStyleBackColor = true; + downloadXmlButton.Click += downloadXmlButton_Click; + // // Object_Spy // AutoScaleDimensions = new SizeF(7F, 15F); @@ -273,6 +285,7 @@ private void InitializeComponent() AutoSizeMode = AutoSizeMode.GrowAndShrink; BackColor = SystemColors.ControlLightLight; ClientSize = new Size(1214, 592); + Controls.Add(downloadXmlButton); Controls.Add(helpButton); Controls.Add(coordLabel); Controls.Add(filterLabel); @@ -328,5 +341,6 @@ private void InitializeComponent() private ToolStripMenuItem addUniqueXpathToFilterToolStripMenuItem1; private Label coordLabel; private Button helpButton; + private Button downloadXmlButton; } } \ No newline at end of file diff --git a/Appium Wizard/Object Spy.cs b/Appium Wizard/Object Spy.cs index fe5bdf2..4138f2e 100644 --- a/Appium Wizard/Object Spy.cs +++ b/Appium Wizard/Object Spy.cs @@ -1211,6 +1211,37 @@ private void helpButton_Click(object sender, EventArgs e) MessageBox.Show(message, "Object Spy - BETA", MessageBoxButtons.OK, MessageBoxIcon.Information); } + private void downloadXmlButton_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(xmlContent)) + { + MessageBox.Show("No XML content available to save. Please refresh the screen first.", "No Content", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + using (SaveFileDialog saveFileDialog = new SaveFileDialog()) + { + saveFileDialog.Filter = "XML files (*.xml)|*.xml|All files (*.*)|*.*"; + saveFileDialog.DefaultExt = "xml"; + saveFileDialog.FileName = $"page_source_{DateTime.Now:yyyyMMdd_HHmmss}.xml"; + saveFileDialog.Title = "Save XML Hierarchy"; + + if (saveFileDialog.ShowDialog() == DialogResult.OK) + { + try + { + File.WriteAllText(saveFileDialog.FileName, xmlContent); + MessageBox.Show($"XML saved successfully to:\n{saveFileDialog.FileName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + Logger.Error(ex, "Error saving XML file"); + MessageBox.Show($"Error saving XML file:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } + private void Object_Spy_FormClosing(object sender, FormClosingEventArgs e) { if (isAndroid) diff --git a/Appium Wizard/OpenDevice.cs b/Appium Wizard/OpenDevice.cs index a1dddf2..de90a7b 100644 --- a/Appium Wizard/OpenDevice.cs +++ b/Appium Wizard/OpenDevice.cs @@ -339,6 +339,13 @@ await Task.Run(() => } commonProgress.UpdateStepLabel(title, "Mounting developer disk image. Please wait, this may take some time...", 60); var output = iOSMethods.GetInstance().MountImage(udid); + + // Handle personalized DDI download for iOS 17+/26+ + if (output.Contains("Downloading personalized DDI")) + { + commonProgress.UpdateStepLabel(title, "Downloading personalized developer disk image for iOS 17+/26+. This may take a few minutes...", 65); + } + if (output.Contains("MountImage: failed to get signature from Apple")) { commonProgress.Close(); @@ -346,9 +353,36 @@ await Task.Run(() => MessageBox.Show("Mounting image failed. Please check your internet connection and try again.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } + + // Check for identity mismatch error (fallback if personalized DDI also failed) + if ((output.Contains("could not find identity") || output.Contains("findIdentity: failed")) && output.Contains("Failed to download personalized DDI")) + { + commonProgress.Close(); + isScreenServerStarted = false; + MessageBox.Show("Failed to mount developer disk image. The personalized DDI download failed.\nPlease check your internet connection and try again.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!iOSMethods.GetInstance().isImageMounted(udid)) { - iOSMethods.GetInstance().MountImage(udid); + Logger.Info("Image not mounted after first attempt, trying again..."); + commonProgress.UpdateStepLabel(title, "Developer disk image not mounted. Retrying mount...", 62); + var retryOutput = iOSMethods.GetInstance().MountImage(udid); + Logger.Info("Retry mount output: " + retryOutput); + + // Wait a bit for mount to complete + Thread.Sleep(2000); + + // Check again if mounted + if (!iOSMethods.GetInstance().isImageMounted(udid)) + { + commonProgress.Close(); + isScreenServerStarted = false; + Logger.Error("Failed to mount developer disk image after retry. Output: " + retryOutput); + MessageBox.Show("Failed to mount developer disk image.\n\nPossible causes:\n1. Device was restarted (DDI gets unmounted on restart)\n2. Internet connection issue (for iOS 17+/26+ personalized DDI)\n3. DDI files corrupted\n\nSolution:\n- Ensure device is unlocked\n- Check internet connection\n- Try again\n\nIf issue persists, delete the folder:\n" + FilesPath.devImagesPath + "iOS_DDI\nand try again to re-download the DDI.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + Logger.Info("Image mounted successfully after retry"); } if (deviceVersion >= version17Plus) { @@ -374,8 +408,12 @@ await Task.Run(() => MessageBox.Show("Please enable Developer Mode in your " + deviceName + " and try again.\nGo to Settings->Privacy & Security->Developer Mode->Turn ON.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } - commonProgress.UpdateStepLabel(title, "Starting WebDriverAgent... Please enter passcode in your iPhone, if it asks...\nOnce you see Automation Running, Go to home screen to reduce the retry.", 70); - WDAsessionId = iOSAsyncMethods.GetInstance().RunWebDriverAgent(commonProgress, udid, proxyPort); + commonProgress.UpdateStepLabel(title, "Starting WebDriverAgent, Please enter passcode if it asks...", 70); + + // Use runwda command directly (more stable than launch command) + Logger.Info("Using runwda command to start WebDriverAgent"); + WDAsessionId = iOSAsyncMethods.GetInstance().RunWebDriverAgentWithRunWDA(commonProgress, udid, proxyPort); + Logger.Info("WDAsessionId:" + WDAsessionId); if (WDAsessionId.Equals("Enable Developer Mode")) { @@ -402,7 +440,7 @@ await Task.Run(() => { commonProgress.Close(); isScreenServerStarted = false; - MessageBox.Show("Unable to launch WebDriverAgent on your iPhone. Please enter passcode on your " + deviceName + " when it asks.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show("Unable to launch WebDriverAgent using runwda command.\n\nPlease:\n1. Enter passcode on your " + deviceName + " when it asks\n2. Ensure Developer Mode is enabled\n3. Unlock your device before opening\n\nNote: If you see WDA running on your device, try going to the home screen and reopening the device.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } else if (string.IsNullOrEmpty(WDAsessionId) | WDAsessionId.Equals("unhandled")) diff --git a/Appium Wizard/Resources/iOS/ddi-downloader.exe b/Appium Wizard/Resources/iOS/ddi-downloader.exe new file mode 100644 index 0000000..312e013 Binary files /dev/null and b/Appium Wizard/Resources/iOS/ddi-downloader.exe differ diff --git a/Appium Wizard/ScreenControl.cs b/Appium Wizard/ScreenControl.cs index 65df387..f05e150 100644 --- a/Appium Wizard/ScreenControl.cs +++ b/Appium Wizard/ScreenControl.cs @@ -731,6 +731,20 @@ private void ScreenControl_FormClosing(object sender, FormClosingEventArgs e) } udidScreenControl.Remove(udid); webview2.Remove(udid); + + // Stop runwda process if running for this device + if (OSType.Equals("iOS")) + { + try + { + iOSAsyncMethods.GetInstance().StopRunWDAProcess(udid); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to stop RunWDA process during form closing"); + } + } + //Hide(); //try //{ diff --git a/Appium Wizard/USBWatcher.cs b/Appium Wizard/USBWatcher.cs index f668764..1e60ef4 100644 --- a/Appium Wizard/USBWatcher.cs +++ b/Appium Wizard/USBWatcher.cs @@ -258,12 +258,13 @@ private void UsbDeviceDisconnected(object sender, EventArrivedEventArgs e) Database.UpdateDataInDevicesTable(udid, "Connection", ""); } } - if (ScreenControl.webview2.ContainsKey(udid)) - { - var control = ScreenControl.udidScreenControl[udid]; - control.LoadDeviceDisconnected(udid); - } - else if (ScreenControl.udidScreenControl.ContainsKey(udid)) + //if (ScreenControl.webview2.ContainsKey(udid)) + //{ + // var control = ScreenControl.udidScreenControl[udid]; + // control.LoadDeviceDisconnected(udid); + //} + //else if (ScreenControl.udidScreenControl.ContainsKey(udid)) + if (ScreenControl.udidScreenControl.ContainsKey(udid)) { var control = ScreenControl.udidScreenControl[udid]; control.Close(); diff --git a/Appium Wizard/VersionInfo.cs b/Appium Wizard/VersionInfo.cs index c4ef12e..c3b1140 100644 --- a/Appium Wizard/VersionInfo.cs +++ b/Appium Wizard/VersionInfo.cs @@ -2,7 +2,7 @@ { public static class VersionInfo { - public const string VersionNumber = "8.6.0"; - public const string ReleaseNotes = "Server settings, screen control, download wda, main screen close update"; + public const string VersionNumber = "8.6.1"; + public const string ReleaseNotes = "Some iPhones not working fix"; } } diff --git a/Appium Wizard/iOSMethods.cs b/Appium Wizard/iOSMethods.cs index 603d157..ed0df73 100644 --- a/Appium Wizard/iOSMethods.cs +++ b/Appium Wizard/iOSMethods.cs @@ -919,7 +919,83 @@ public string MountImage(string udid) { if (isGo) { - return ExecuteCommand("image auto", udid); + // First, check if personalized DDI already exists (for iOS 17+/26+) + string personalizedDdiPath = FilesPath.devImagesPath + "iOS_DDI\\Restore"; + bool hasPersonalizedDDI = Directory.Exists(personalizedDdiPath); + + if (hasPersonalizedDDI) + { + // Personalized DDI exists, check if it's valid + var dmgFiles = Directory.GetFiles(personalizedDdiPath, "*.dmg"); + var plistFiles = Directory.GetFiles(personalizedDdiPath, "BuildManifest.plist"); + + if (dmgFiles.Length > 0 && plistFiles.Length > 0) + { + Logger.Info("Found existing personalized DDI at: " + personalizedDdiPath + ". Attempting to mount directly."); + + // Try mounting the personalized DDI directly + string output = ExecuteCommand("image mount --path=\"" + personalizedDdiPath + "\"", udid); + + if (output.Contains("\"level\":\"info\",\"msg\":\"ok\"") || output.Contains("successfully mounted")) + { + Logger.Info("Personalized DDI mounted successfully"); + return output; + } + else if (output.Contains("DeviceLocked")) + { + Logger.Info("Device is locked. Please unlock and retry."); + return output; + } + else + { + Logger.Warn("Failed to mount existing personalized DDI. Output: " + output); + // Continue to try image auto as fallback + } + } + else + { + Logger.Warn("Personalized DDI folder exists but files are incomplete. Will try to re-download."); + } + } + + // Try standard image auto command + Logger.Info("Trying standard image auto command"); + string autoOutput = ExecuteCommand("image auto", udid); + + // Check if it succeeded + if (autoOutput.Contains("\"level\":\"info\",\"msg\":\"ok\"") || autoOutput.Contains("successfully mounted")) + { + Logger.Info("Standard DDI mounted successfully"); + return autoOutput; + } + + // Check if mounting failed due to identity mismatch (iOS 17+ / iOS 26.3+ personalized DDI issue) + if (autoOutput.Contains("could not find identity") || autoOutput.Contains("findIdentity: failed")) + { + Logger.Info("Standard DDI mounting failed with identity mismatch. Attempting to download personalized DDI for iOS 17+/26+..."); + + // Download personalized DDI using ddi-downloader.exe + personalizedDdiPath = DownloadPersonalizedDDI(); + + if (!string.IsNullOrEmpty(personalizedDdiPath) && Directory.Exists(personalizedDdiPath)) + { + Logger.Info("Personalized DDI downloaded. Attempting to mount from: " + personalizedDdiPath); + + // Mount the personalized DDI + autoOutput = ExecuteCommand("image mount --path=\"" + personalizedDdiPath + "\"", udid); + + if (autoOutput.Contains("DeviceLocked")) + { + Logger.Info("Device is locked. Please unlock and retry."); + } + } + else + { + Logger.Error("Failed to download personalized DDI"); + } + } + + return autoOutput; } else { @@ -932,6 +1008,111 @@ public string MountImage(string udid) } } + private string DownloadPersonalizedDDI() + { + try + { + if (!File.Exists(FilesPath.ddiDownloaderFilePath)) + { + Logger.Error("ddi-downloader.exe not found at: " + FilesPath.ddiDownloaderFilePath); + return null; + } + + // Ensure devimages directory exists + if (!Directory.Exists(FilesPath.devImagesPath)) + { + Directory.CreateDirectory(FilesPath.devImagesPath); + } + + string outputPath = FilesPath.devImagesPath + "iOS_DDI"; + + // If DDI already exists, check if it's valid before re-downloading + string restorePath = Path.Combine(outputPath, "Restore"); + if (Directory.Exists(restorePath)) + { + // Check if required files exist + var dmgFiles = Directory.GetFiles(restorePath, "*.dmg"); + var plistFiles = Directory.GetFiles(restorePath, "BuildManifest.plist"); + + if (dmgFiles.Length > 0 && plistFiles.Length > 0) + { + Logger.Info("Personalized DDI already exists at: " + restorePath); + return restorePath; + } + else + { + Logger.Info("Existing DDI is incomplete, re-downloading..."); + Directory.Delete(outputPath, true); + } + } + + Logger.Info("Downloading personalized DDI to: " + outputPath); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = FilesPath.ddiDownloaderFilePath, + Arguments = "--output \"" + outputPath + "\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = FilesPath.iOSFilesPath + }; + + Process process = new Process(); + process.StartInfo = startInfo; + process.Start(); + + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + + process.WaitForExit(120000); // Wait up to 2 minutes for download + + Logger.Info("DDI Downloader output: " + output); + + if (!string.IsNullOrEmpty(error)) + { + Logger.Error("DDI Downloader error: " + error); + } + + if (process.ExitCode != 0) + { + Logger.Error("DDI Downloader exited with code: " + process.ExitCode); + return null; + } + + // The ddi-downloader creates the structure compatible with go-ios + // It should create: iOS_DDI/Restore/ with the necessary files + if (Directory.Exists(restorePath)) + { + // Verify the required files exist + var dmgFiles = Directory.GetFiles(restorePath, "*.dmg"); + var plistFiles = Directory.GetFiles(restorePath, "BuildManifest.plist"); + + if (dmgFiles.Length > 0 && plistFiles.Length > 0) + { + Logger.Info("Successfully downloaded personalized DDI with " + dmgFiles.Length + " DMG file(s)"); + return restorePath; + } + else + { + Logger.Error("DDI download completed but required files (DMG/BuildManifest) not found"); + return null; + } + } + else + { + Logger.Error("DDI download completed but Restore folder not found"); + return null; + } + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to download personalized DDI"); + return null; + } + } + public bool isImageMounted(string udid) { var output = ExecuteCommand("image list", udid); @@ -1403,6 +1584,7 @@ public class iOSAsyncMethods private Process pyAsyncProcess; private StringBuilder outputBuffer; public static Dictionary? PortProcessId; + public static Dictionary? RunWDAProcesses; // Tracks runwda processes by UDID public enum iOSExecutable { go, py } public static bool isGo; public static bool is17Plus; @@ -1411,6 +1593,7 @@ public iOSAsyncMethods() { InitializeProcess(); PortProcessId = new Dictionary(); + RunWDAProcesses = new Dictionary(); } public static iOSAsyncMethods GetInstance() @@ -1782,7 +1965,7 @@ public string RunWebDriverAgentQuick(CommonProgress commonProgress, string udid, { break; } - commonProgress.UpdateStepLabel("Restarting WebDriverAgentRunner...\nRetry " + count + "/5."); + commonProgress.UpdateStepLabel("Restarting WebDriverAgentRunner, Please enter passcode if it asks...\nRetry " + count + "/5."); iOSMethods.GetInstance().KillWDA(udid); iOSMethods.GetInstance().RunWebDriverAgentQuick(udid); commonProgress.UpdateStepLabel("Please enter Passcode on your iPhone if it asks...\nOnce you see Automation Running, Go to home screen to reduce the retry.\nRetry " + count + "/5."); @@ -1806,6 +1989,190 @@ public string RunWebDriverAgentQuick(CommonProgress commonProgress, string udid, } } + /// + /// Runs WebDriverAgent using the 'runwda' command which keeps the process running continuously. + /// This is more stable than 'launch' command for devices where WDA closes after a few seconds. + /// The process is tracked and can be killed when the device is closed. + /// + public string RunWebDriverAgentWithRunWDA(CommonProgress commonProgress, string udid, int port) + { + try + { + Logger.Info("RunWebDriverAgentWithRunWDA called for UDID: " + udid); + + // Check if WDA is already running + string sessionId = iOSMethods.GetInstance().IsWDARunning(port); + if (!sessionId.Equals("nosession")) + { + Logger.Info("WDA already running with session: " + sessionId); + return sessionId; + } + + // Check if runwda process is already running for this device + if (RunWDAProcesses != null && RunWDAProcesses.ContainsKey(udid)) + { + Process existingProcess = RunWDAProcesses[udid]; + if (existingProcess != null && !existingProcess.HasExited) + { + Logger.Info("RunWDA process already running for UDID: " + udid); + // Wait for WDA to become available + sessionId = WaitForWDASession(port, 30); + return sessionId; + } + else + { + // Process has exited, remove it + RunWDAProcesses.Remove(udid); + } + } + + // Check if WDA is installed + bool isInstalled = iOSMethods.GetInstance().iSWDAInstalled(udid); + if (!isInstalled) + { + Logger.Info("WDA not installed for UDID: " + udid); + return "WDA Not Installed"; + } + + // Create and start the runwda process + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = FilesPath.iOSServerFilePath, + Arguments = "runwda --udid=" + udid + " --env USE_PORT=" + "8100", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + Process runWDAProcess = new Process(); + runWDAProcess.StartInfo = startInfo; + + // Set up output handlers to monitor WDA status + runWDAProcess.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Logger.Debug("RunWDA Output: " + e.Data); + if (e.Data.Contains("authorized\":true") || e.Data.Contains("ServerURLHere")) + { + Logger.Info("WDA authorization detected"); + } + } + }; + + runWDAProcess.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Logger.Debug("RunWDA Error: " + e.Data); + if (e.Data.Contains("Could not start service")) + { + Logger.Error("Developer mode disabled"); + } + } + }; + + Logger.Info("Starting runwda process for UDID: " + udid); + runWDAProcess.Start(); + runWDAProcess.BeginOutputReadLine(); + runWDAProcess.BeginErrorReadLine(); + + // Store the process reference + if (RunWDAProcesses == null) + { + RunWDAProcesses = new Dictionary(); + } + RunWDAProcesses[udid] = runWDAProcess; + + Logger.Info("RunWDA process started with PID: " + runWDAProcess.Id); + + // Wait for WDA to start and create session + //commonProgress.UpdateStepLabel("Waiting for WebDriverAgent to start using runwda command...", 75); + sessionId = WaitForWDASession(port, 30); + + if (sessionId.Equals("nosession")) + { + Logger.Warn("Failed to get WDA session after starting runwda"); + // Check if process is still running + if (runWDAProcess.HasExited) + { + Logger.Error("RunWDA process exited unexpectedly with code: " + runWDAProcess.ExitCode); + RunWDAProcesses.Remove(udid); + } + } + else + { + Logger.Info("Successfully got WDA session: " + sessionId); + iOSAPIMethods.GoToHome(port); + } + + return sessionId; + } + catch (Exception ex) + { + Logger.Error(ex, "Exception in RunWebDriverAgentWithRunWDA"); + return "unhandled"; + } + } + + /// + /// Waits for WDA to become available and returns the session ID + /// + private string WaitForWDASession(int port, int timeoutSeconds) + { + string sessionId = "nosession"; + int attempts = timeoutSeconds / 2; // Check every 2 seconds + int count = 0; + + while (sessionId.Equals("nosession") && count < attempts) + { + Thread.Sleep(2000); + sessionId = iOSAPIMethods.CreateWDASession(port); + if (!sessionId.Equals("nosession")) + { + Logger.Info("WDA session created: " + sessionId); + break; + } + count++; + Logger.Debug("Waiting for WDA session... Attempt " + (count + 1) + "/" + attempts); + } + + if (sessionId.Equals("nosession")) + { + // Try one more time to check if WDA is already running + sessionId = iOSMethods.GetInstance().IsWDARunning(port); + } + + return sessionId; + } + + /// + /// Stops the runwda process for a specific device + /// + public void StopRunWDAProcess(string udid) + { + try + { + if (RunWDAProcesses != null && RunWDAProcesses.ContainsKey(udid)) + { + Process process = RunWDAProcesses[udid]; + if (process != null && !process.HasExited) + { + Logger.Info("Stopping RunWDA process for UDID: " + udid); + process.Kill(); + process.WaitForExit(5000); + Logger.Info("RunWDA process stopped"); + } + RunWDAProcesses.Remove(udid); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to stop RunWDA process for UDID: " + udid); + } + } + Process tunnelProcess; public bool CreateTunnelPy() {