From dcd930cf89c5a09515505bf69f47632c13b4de26 Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sat, 11 Apr 2026 22:19:41 +0530 Subject: [PATCH 01/12] Fixed execution speed issue --- Appium Wizard/AppiumServerSetup.cs | 83 ++++++++++++++++++- Appium Wizard/ExecutionStatus.cs | 128 +++++++++++++++++++++-------- Appium Wizard/ScreenControl.cs | 69 +++++++++++++--- 3 files changed, 232 insertions(+), 48 deletions(-) diff --git a/Appium Wizard/AppiumServerSetup.cs b/Appium Wizard/AppiumServerSetup.cs index e99f588..f537ee2 100644 --- a/Appium Wizard/AppiumServerSetup.cs +++ b/Appium Wizard/AppiumServerSetup.cs @@ -3,6 +3,7 @@ using NLog; using RestSharp; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; @@ -20,6 +21,10 @@ public class AppiumServerSetup public static bool UpdateStatusInScreenFlag = true; public static Dictionary serverNumberWDAPortNumber = new Dictionary(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + // Cache for element rect values - reduces HTTP calls + private static Dictionary, long>> rectCache = new Dictionary, long>>(); + private static readonly object rectCacheLock = new object(); string appiumLogLevel, updatedCommand; public void StartAppiumServer(int appiumPort, int serverNumber, string command = "appium --allow-cors --allow-insecure=adb_shell") { @@ -651,18 +656,62 @@ public static string GetAndroidIdFromAppiumServer(int appiumPort, string appiumS } } - public static Dictionary ElementInfo(string url, string elementId) + // True Async HTTP with Caching - Non-blocking + public static async Task> ElementInfoAsync(string url, string elementId) { Dictionary rectangleDict = new Dictionary(); + + // OPTION 4: Check cache first (1.5 second expiration) + string cacheKey = $"{url}_{elementId}"; + long currentTicks = DateTime.Now.Ticks; + + lock (rectCacheLock) + { + if (rectCache.ContainsKey(cacheKey)) + { + var cached = rectCache[cacheKey]; + long cachedTicks = cached.Item2; + double ageSeconds = new TimeSpan(currentTicks - cachedTicks).TotalSeconds; + + // Return cached value if less than 1.5 seconds old + if (ageSeconds < 1.5) + { + System.Diagnostics.Debug.WriteLine($"ElementInfo - Cache HIT for {elementId} (age: {ageSeconds:F2}s)"); + return cached.Item1; + } + else + { + // Remove stale entry + rectCache.Remove(cacheKey); + System.Diagnostics.Debug.WriteLine($"ElementInfo - Cache EXPIRED for {elementId} (age: {ageSeconds:F2}s)"); + } + } + + // Clean up very old cache entries (> 5 seconds) + var expiredKeys = rectCache.Where(kvp => new TimeSpan(currentTicks - kvp.Value.Item2).TotalSeconds > 5) + .Select(kvp => kvp.Key) + .ToList(); + foreach (var key in expiredKeys) + { + rectCache.Remove(key); + } + } + + // Cache miss - make HTTP call + System.Diagnostics.Debug.WriteLine($"ElementInfo - Cache MISS for {elementId}, making HTTP call"); + try { var options = new RestClientOptions(url) { - //Timeout = TimeSpan.FromSeconds(5), + MaxTimeout = 500 // 500ms timeout }; var client = new RestClient(options); var request = new RestRequest(elementId + "/rect", Method.Get); - RestResponse response = client.Execute(request); + + // TRUE ASYNC - Does not block thread pool + RestResponse response = await client.ExecuteAsync(request); + if (response.StatusCode == HttpStatusCode.OK && response.Content != null) { var jsonObject = JsonConvert.DeserializeObject(response.Content); @@ -678,15 +727,41 @@ public static Dictionary ElementInfo(string url, string elementId) { "width", width }, { "height", height } }; + + // Store in cache with current timestamp + lock (rectCacheLock) + { + rectCache[cacheKey] = new Tuple, long>(rectangleDict, currentTicks); + System.Diagnostics.Debug.WriteLine($"ElementInfo - Cached rect for {elementId}"); + } } return rectangleDict; } - catch (Exception) + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"ElementInfoAsync failed: {ex.Message}"); return rectangleDict; } } + // Keep sync version for backward compatibility (if used elsewhere) + public static Dictionary ElementInfo(string url, string elementId) + { + // Use Task.Run to avoid potential deadlocks from sync-over-async + return Task.Run(() => ElementInfoAsync(url, elementId)).GetAwaiter().GetResult(); + } + + // Clear the rect cache (useful when screen changes significantly) + public static void ClearRectCache() + { + lock (rectCacheLock) + { + int count = rectCache.Count; + rectCache.Clear(); + System.Diagnostics.Debug.WriteLine($"Cleared rect cache ({count} entries)"); + } + } + public static bool isPortReachable(int port) { var options = new RestClientOptions("http://localhost:" + port) diff --git a/Appium Wizard/ExecutionStatus.cs b/Appium Wizard/ExecutionStatus.cs index cce9add..7879c03 100644 --- a/Appium Wizard/ExecutionStatus.cs +++ b/Appium Wizard/ExecutionStatus.cs @@ -17,6 +17,7 @@ public class ExecutionStatus static string url = ""; int x, y, width, height, screenDensity; ScreenControl screenControl; + private static long lastRectCallTicks = 0; // Thread-safe debouncing public void UpdateScreenControl(ScreenControl screenControl, string data) { @@ -36,18 +37,19 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) if (match.Success) { url = match.Value; + System.Diagnostics.Debug.WriteLine($"Extracted URL: {url}"); } string json = GetOnlyJson(data); try { - if (IsValidJson(json)) + if (!string.IsNullOrEmpty(json) && json.Length > 2) { var dictionary = JsonConvert.DeserializeObject>(json); - if (dictionary.ContainsKey("selector")) + if (dictionary != null && dictionary.ContainsKey("selector")) { element = dictionary["selector"]; } - else if (dictionary.ContainsKey("value")) + else if (dictionary != null && dictionary.ContainsKey("value")) { element = dictionary["value"]; } @@ -55,8 +57,9 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) screenControl.UpdateStatusLabel(screenControl, statusText); } } - catch (Exception) + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"Find element parsing error: {ex.Message}"); } } // Handling for Click @@ -76,7 +79,8 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) string json = GetOnlyJson(data); try { - if (IsValidJson(json)) + // Skip double JSON parsing - just try to deserialize directly + if (!string.IsNullOrEmpty(json) && json.Length > 2) { var jsonObject = JsonConvert.DeserializeObject(json); text = jsonObject.text; @@ -253,18 +257,36 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) { try { - if (data.Contains("value") && data.Contains("ELEMENT") && data.Contains("sessionId")) + if (data.Contains("value") && (data.Contains("ELEMENT") || data.Contains("element-6066"))) { string json = GetOnlyJson(data); - JObject jsonObject = JObject.Parse(json); - string elementId = jsonObject["value"]["ELEMENT"].ToString(); - GetRectValues(url, elementId); + if (!string.IsNullOrEmpty(json) && json.Length > 10) + { + JObject jsonObject = JObject.Parse(json); + string elementId = null; + + // Try JSONWP format first (old format) + if (jsonObject["value"]?["ELEMENT"] != null) + { + elementId = jsonObject["value"]["ELEMENT"].ToString(); + } + // Try W3C format (new format) + else if (jsonObject["value"]?["element-6066-11e4-a52e-4f735466cecf"] != null) + { + elementId = jsonObject["value"]["element-6066-11e4-a52e-4f735466cecf"].ToString(); + } + + if (!string.IsNullOrEmpty(elementId) && !string.IsNullOrEmpty(url)) + { + GetRectValues(url, elementId); + } + } } screenControl.UpdateStatusLabel(screenControl, ""); } - catch (Exception) + catch (Exception ex) { - + System.Diagnostics.Debug.WriteLine($"Response parsing failed: {ex.Message}"); } } } @@ -275,42 +297,80 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) } - private void GetRectValues(string url, string elementId) + private async void GetRectValues(string url, string elementId) { - var result = AppiumServerSetup.ElementInfo(url, elementId); - if (result.Count != 0 && result != null) + // Smart Debouncing - Skip if called too frequently + long currentTicks = DateTime.Now.Ticks; + long lastTicks = Interlocked.Read(ref lastRectCallTicks); + + // Skip if called within 150ms (prevents pile-up during rapid element finds) + if (new TimeSpan(currentTicks - lastTicks).TotalMilliseconds < 150) { - if (screenControl.isAndroid) - { - x = (int)(result["x"] / (screenDensity / 160f)); - y = (int)(result["y"] / (screenDensity / 160f)); - width = (int)(result["width"] / (screenDensity / 160f)); - height = (int)(result["height"] / (screenDensity / 160f)); - } - else + System.Diagnostics.Debug.WriteLine($"GetRectValues skipped - debouncing"); + return; + } + + Interlocked.Exchange(ref lastRectCallTicks, currentTicks); + + System.Diagnostics.Debug.WriteLine($"GetRectValues called - URL: {url}, ElementID: {elementId}"); + + try + { + // True Async - no thread blocking + var result = await AppiumServerSetup.ElementInfoAsync(url, elementId); + System.Diagnostics.Debug.WriteLine($"ElementInfo result count: {result?.Count}"); + + if (result != null && result.Count != 0) { - x = result["x"]; - y = result["y"]; - width = result["width"]; - height = result["height"]; + if (screenControl.isAndroid) + { + x = (int)(result["x"] / (screenDensity / 160f)); + y = (int)(result["y"] / (screenDensity / 160f)); + width = (int)(result["width"] / (screenDensity / 160f)); + height = (int)(result["height"] / (screenDensity / 160f)); + } + else + { + x = result["x"]; + y = result["y"]; + width = result["width"]; + height = result["height"]; + } + System.Diagnostics.Debug.WriteLine($"Drawing rectangle at: {x},{y} size: {width}x{height}"); + screenControl.DrawRectangle(screenControl, x, y, width, height); } - screenControl.DrawRectangle(screenControl, x, y, width, height); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"GetRectValues exception: {ex.Message}\n{ex.StackTrace}"); } } public static string GetOnlyJson(string text) { - Match match = Regex.Match(text, @"\{.*\}"); - string output; - if (match.Success) + // Fast string-based extraction instead of regex (much faster for UiSelector) + int firstBrace = text.IndexOf('{'); + if (firstBrace == -1) { - output = match.Value; + return "No JSON found in the string"; } - else + + // Find matching closing brace + int braceCount = 0; + for (int i = firstBrace; i < text.Length; i++) { - output = "No JSON found in the string"; + if (text[i] == '{') braceCount++; + else if (text[i] == '}') + { + braceCount--; + if (braceCount == 0) + { + return text.Substring(firstBrace, i - firstBrace + 1); + } + } } - return output; + + return "No JSON found in the string"; } public static string GetValueFromJson(string data, string expected) diff --git a/Appium Wizard/ScreenControl.cs b/Appium Wizard/ScreenControl.cs index f05e150..5cc643d 100644 --- a/Appium Wizard/ScreenControl.cs +++ b/Appium Wizard/ScreenControl.cs @@ -37,6 +37,8 @@ public partial class ScreenControl : Form public string deviceSerialNumber; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public bool useScrcpy = false; + private DateTime lastStatusUpdateTime = DateTime.MinValue; + private string lastStatusText = ""; public ScreenControl(string os, string Version, string udid, int width, int height, string session, string selectedDeviceName, int proxyPort, int screenPort, string deviceModel, bool useScrcpy = true) { @@ -140,11 +142,46 @@ public void UpdateStatusLabel(ScreenControl screenControl, string actualText) { try { - BeginInvoke(new Action(() => + // Throttle rapid updates to prevent UI thread overload + // Skip if same text is being set within 30ms (prevents backlog during rapid element finds) + var now = DateTime.Now; + if (actualText == lastStatusText && (now - lastStatusUpdateTime).TotalMilliseconds < 30) { - toolStripStatusLabel.Text = actualText; - toolStripStatusLabel.ToolTipText = actualText; - })); + return; + } + + lastStatusText = actualText; + lastStatusUpdateTime = now; + + // Truncate very long text (like UiSelector) to avoid UI rendering overhead + string displayText = actualText; + string tooltipText = actualText; + if (actualText.Length > 100) + { + displayText = actualText.Substring(0, 97) + "..."; + } + + // Use fire-and-forget pattern to avoid queuing up UI thread + // This prevents backlog when there are rapid element finds + if (!screenControl.IsDisposed && screenControl.IsHandleCreated) + { + try + { + screenControl.BeginInvoke(new Action(() => + { + try + { + if (!toolStripStatusLabel.IsDisposed) + { + toolStripStatusLabel.Text = displayText; + toolStripStatusLabel.ToolTipText = tooltipText; + } + } + catch { } + })); + } + catch { } + } } catch (Exception) { @@ -892,8 +929,12 @@ public async void DrawRectangle(ScreenControl screenControl, int x, int y, int w if (useScrcpy) { DrawRectangleOnScrcpy(x, y, width, height); - await Task.Delay(1000); - ClearDrawing(); + // Non-blocking delay - clear drawing after a short time without blocking execution + _ = Task.Run(async () => + { + await Task.Delay(1000); + ClearDrawing(); + }); } else { @@ -917,8 +958,12 @@ public async void DrawArrow(ScreenControl screenControl, int startX, int startY, if (useScrcpy) { DrawArrowOnScrcpy(startX, startY, endX, endY, 10); - await Task.Delay(1000); - ClearDrawing(); + // Non-blocking delay - clear drawing after a short time without blocking execution + _ = Task.Run(async () => + { + await Task.Delay(1000); + ClearDrawing(); + }); } else { @@ -940,8 +985,12 @@ public async void DrawDot(ScreenControl screenControl, int x, int y) if (useScrcpy) { DrawDotOnScrcpy(x, y, 5); - await Task.Delay(500); - ClearDrawing(); + // Non-blocking delay - clear drawing after a short time without blocking execution + _ = Task.Run(async () => + { + await Task.Delay(500); + ClearDrawing(); + }); } else { From 0db6961692aa02c98e5fa55b839dfe4ede938efa Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sat, 11 Apr 2026 22:42:22 +0530 Subject: [PATCH 02/12] Added option in screen control to disable status text and drawing --- Appium Wizard/ScreenControl.Designer.cs | 40 +++++++++++++++++- Appium Wizard/ScreenControl.cs | 55 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Appium Wizard/ScreenControl.Designer.cs b/Appium Wizard/ScreenControl.Designer.cs index 461d1b3..91d9c21 100644 --- a/Appium Wizard/ScreenControl.Designer.cs +++ b/Appium Wizard/ScreenControl.Designer.cs @@ -52,6 +52,9 @@ private void InitializeComponent() copyScreenPortToolStripMenuItem1 = new ToolStripMenuItem(); copySessionIDToolStripMenuItem1 = new ToolStripMenuItem(); copySessionURLToolStripMenuItem1 = new ToolStripMenuItem(); + executionStatusSettingsToolStripMenuItem = new ToolStripMenuItem(); + showStatusTextToolStripMenuItem = new ToolStripMenuItem(); + showDrawingToolStripMenuItem = new ToolStripMenuItem(); objectSpyButton = new ToolStripButton(); RecordAndStopRecordingSteps = new ToolStripSplitButton(); playStepsToolStripMenuItem = new ToolStripMenuItem(); @@ -159,7 +162,7 @@ private void InitializeComponent() // MoreToolStripButton // MoreToolStripButton.DisplayStyle = ToolStripItemDisplayStyle.Image; - MoreToolStripButton.DropDownItems.AddRange(new ToolStripItem[] { UnlockScreen, manageAppsToolStripMenuItem, deviceInfoToolStripMenuItem, infoToolStripMenuItem }); + MoreToolStripButton.DropDownItems.AddRange(new ToolStripItem[] { UnlockScreen, manageAppsToolStripMenuItem, deviceInfoToolStripMenuItem, infoToolStripMenuItem, executionStatusSettingsToolStripMenuItem }); MoreToolStripButton.Image = Properties.Resources.ellipsis; MoreToolStripButton.ImageTransparentColor = Color.Magenta; MoreToolStripButton.Name = "MoreToolStripButton"; @@ -277,7 +280,37 @@ private void InitializeComponent() copySessionURLToolStripMenuItem1.Size = new Size(168, 22); copySessionURLToolStripMenuItem1.Text = "Copy Session URL"; copySessionURLToolStripMenuItem1.Click += copySessionURLToolStripMenuItem_Click; - // + // + // executionStatusSettingsToolStripMenuItem + // + executionStatusSettingsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { showStatusTextToolStripMenuItem, showDrawingToolStripMenuItem }); + executionStatusSettingsToolStripMenuItem.Name = "executionStatusSettingsToolStripMenuItem"; + executionStatusSettingsToolStripMenuItem.Size = new Size(225, 22); + executionStatusSettingsToolStripMenuItem.Text = "Execution Status Settings"; + executionStatusSettingsToolStripMenuItem.ToolTipText = "Configure visual feedback during test execution"; + // + // showStatusTextToolStripMenuItem + // + showStatusTextToolStripMenuItem.Checked = true; + showStatusTextToolStripMenuItem.CheckOnClick = true; + showStatusTextToolStripMenuItem.CheckState = CheckState.Checked; + showStatusTextToolStripMenuItem.Name = "showStatusTextToolStripMenuItem"; + showStatusTextToolStripMenuItem.Size = new Size(175, 22); + showStatusTextToolStripMenuItem.Text = "Show Status Text"; + showStatusTextToolStripMenuItem.ToolTipText = "Display execution status text in the status bar. Disable this if you experience delays during test execution."; + showStatusTextToolStripMenuItem.Click += showStatusTextToolStripMenuItem_Click; + // + // showDrawingToolStripMenuItem + // + showDrawingToolStripMenuItem.Checked = true; + showDrawingToolStripMenuItem.CheckOnClick = true; + showDrawingToolStripMenuItem.CheckState = CheckState.Checked; + showDrawingToolStripMenuItem.Name = "showDrawingToolStripMenuItem"; + showDrawingToolStripMenuItem.Size = new Size(175, 22); + showDrawingToolStripMenuItem.Text = "Show Drawing"; + showDrawingToolStripMenuItem.ToolTipText = "Show visual feedback (rectangles, dots, arrows) on screen during element interactions. Disable this if you experience delays during test execution."; + showDrawingToolStripMenuItem.Click += showDrawingToolStripMenuItem_Click; + // // objectSpyButton // objectSpyButton.Alignment = ToolStripItemAlignment.Right; @@ -427,6 +460,9 @@ private void InitializeComponent() private ToolStripMenuItem copyAllInfoToolStripMenuItem; private ToolStripMenuItem copyUDIDToolStripMenuItem; private ToolStripMenuItem showDeviceInfoToolStripMenuItem; + private ToolStripMenuItem executionStatusSettingsToolStripMenuItem; + private ToolStripMenuItem showStatusTextToolStripMenuItem; + private ToolStripMenuItem showDrawingToolStripMenuItem; private ToolStripSplitButton navigationHomeSplitButton; private ToolStripMenuItem recentToolStripMenuItem; private ToolStripMenuItem backToolStripMenuItem; diff --git a/Appium Wizard/ScreenControl.cs b/Appium Wizard/ScreenControl.cs index 5cc643d..7be7731 100644 --- a/Appium Wizard/ScreenControl.cs +++ b/Appium Wizard/ScreenControl.cs @@ -40,6 +40,10 @@ public partial class ScreenControl : Form private DateTime lastStatusUpdateTime = DateTime.MinValue; private string lastStatusText = ""; + // Per-instance execution status settings + public bool ShowExecutionStatusText { get; set; } = true; + public bool ShowExecutionDrawing { get; set; } = true; + public ScreenControl(string os, string Version, string udid, int width, int height, string session, string selectedDeviceName, int proxyPort, int screenPort, string deviceModel, bool useScrcpy = true) { InitializeComponent(); @@ -142,6 +146,12 @@ public void UpdateStatusLabel(ScreenControl screenControl, string actualText) { try { + // Check if status text is enabled for this instance + if (!screenControl.ShowExecutionStatusText) + { + return; + } + // Throttle rapid updates to prevent UI thread overload // Skip if same text is being set within 30ms (prevents backlog during rapid element finds) var now = DateTime.Now; @@ -926,6 +936,12 @@ public async void DrawRectangle(ScreenControl screenControl, int x, int y, int w { try { + // Check if drawing is enabled for this instance + if (!screenControl.ShowExecutionDrawing) + { + return; + } + if (useScrcpy) { DrawRectangleOnScrcpy(x, y, width, height); @@ -955,6 +971,12 @@ public async void DrawArrow(ScreenControl screenControl, int startX, int startY, { try { + // Check if drawing is enabled for this instance + if (!screenControl.ShowExecutionDrawing) + { + return; + } + if (useScrcpy) { DrawArrowOnScrcpy(startX, startY, endX, endY, 10); @@ -982,6 +1004,12 @@ public async void DrawDot(ScreenControl screenControl, int x, int y) { try { + // Check if drawing is enabled for this instance + if (!screenControl.ShowExecutionDrawing) + { + return; + } + if (useScrcpy) { DrawDotOnScrcpy(x, y, 5); @@ -1938,6 +1966,33 @@ await Task.Run(() => }); } + private void showStatusTextToolStripMenuItem_Click(object sender, EventArgs e) + { + // Toggle the status text display setting for this screen control instance + ShowExecutionStatusText = showStatusTextToolStripMenuItem.Checked; + + // Clear status text if disabled + if (!ShowExecutionStatusText) + { + if (!toolStripStatusLabel.IsDisposed) + { + toolStripStatusLabel.Text = ""; + } + } + } + + private void showDrawingToolStripMenuItem_Click(object sender, EventArgs e) + { + // Toggle the drawing display setting for this screen control instance + ShowExecutionDrawing = showDrawingToolStripMenuItem.Checked; + + // Clear any existing drawings if disabled + if (!ShowExecutionDrawing) + { + ClearDrawing(); + } + } + //<<<------------------------------------------------------------------------------------------------------------>>> } From 19ff1893743a45e720279db416fa8dd9814f10fa Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sat, 11 Apr 2026 23:16:15 +0530 Subject: [PATCH 03/12] Fixes scrcpy blackout issue --- Appium Wizard/ScrcpyEmbedder.cs | 227 +++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 1 deletion(-) diff --git a/Appium Wizard/ScrcpyEmbedder.cs b/Appium Wizard/ScrcpyEmbedder.cs index 5e84b8b..2932033 100644 --- a/Appium Wizard/ScrcpyEmbedder.cs +++ b/Appium Wizard/ScrcpyEmbedder.cs @@ -7,11 +7,14 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Forms; + using NLog; namespace Appium_Wizard { public class ScrcpyEmbedder : IDisposable { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + // Add these Win32 API calls at the top of ScrcpyEmbedder class [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); @@ -79,6 +82,11 @@ static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, private Process scrcpyProcess; private Panel hostPanel; + private System.Windows.Forms.Timer processMonitorTimer; + private string currentUdid; + private int restartAttempts = 0; + private const int MaxRestartAttempts = 5; + private bool isRestarting = false; /// /// Gets the Panel control that hosts the embedded scrcpy window. @@ -118,6 +126,9 @@ public async Task StartAsync(string udid) if (scrcpyProcess != null && !scrcpyProcess.HasExited) throw new InvalidOperationException("Scrcpy is already running."); + // Store the udid for potential restarts + currentUdid = udid; + ScrcpyArguments = ScrcpyArguments + " -s "+udid; ProcessStartInfo psi = new ProcessStartInfo @@ -167,6 +178,9 @@ public async Task StartAsync(string udid) // Embed the scrcpy window inside the panel EmbedWindow(scrcpyHandle); + // Start process monitoring + StartProcessMonitoring(); + return true; } catch (Exception ex) @@ -176,6 +190,194 @@ public async Task StartAsync(string udid) } } + private void StartProcessMonitoring() + { + try + { + processMonitorTimer = new System.Windows.Forms.Timer(); + processMonitorTimer.Interval = 2000; // Check every 2 seconds + processMonitorTimer.Tick += OnProcessMonitorTick; + processMonitorTimer.Start(); + Logger.Info("Scrcpy process monitoring started"); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to start process monitoring"); + } + } + + private void OnProcessMonitorTick(object sender, EventArgs e) + { + try + { + if (scrcpyProcess != null && scrcpyProcess.HasExited && !isRestarting) + { + int exitCode = scrcpyProcess.ExitCode; + Logger.Warn($"Scrcpy process exited. Exit code: {exitCode}. Attempting automatic restart..."); + + // Try to read error output + try + { + string error = scrcpyProcess.StandardError.ReadToEnd(); + if (!string.IsNullOrEmpty(error)) + { + Logger.Warn($"Scrcpy error output: {error}"); + } + } + catch (Exception readEx) + { + Logger.Error(readEx, "Failed to read scrcpy error output"); + } + + // Check if we should attempt restart + if (restartAttempts < MaxRestartAttempts) + { + restartAttempts++; + Logger.Info($"Restart attempt {restartAttempts}/{MaxRestartAttempts}"); + + // Attempt automatic restart + _ = RestartScrcpyAsync(); + } + else + { + // Max restart attempts reached, stop monitoring and show error + Logger.Error($"Max restart attempts ({MaxRestartAttempts}) reached. Stopping automatic restart."); + processMonitorTimer?.Stop(); + + // Change panel color to indicate failure + if (hostPanel != null && !hostPanel.IsDisposed) + { + if (hostPanel.InvokeRequired) + { + hostPanel.Invoke(new Action(() => + { + hostPanel.BackColor = Color.DarkRed; + })); + } + else + { + hostPanel.BackColor = Color.DarkRed; + } + } + + ShowError($"Screen mirroring failed after {MaxRestartAttempts} restart attempts.\n\nPlease check:\n- Device is still connected\n- USB debugging is enabled\n- Device screen is not locked"); + } + } + } + catch (Exception ex) + { + Logger.Error(ex, "Error in process monitoring tick"); + } + } + + private async Task RestartScrcpyAsync() + { + isRestarting = true; + + try + { + Logger.Info("Restarting scrcpy..."); + + // Stop the monitoring timer temporarily + processMonitorTimer?.Stop(); + + // Clean up old process + if (scrcpyProcess != null) + { + try + { + scrcpyProcess.Dispose(); + } + catch { } + scrcpyProcess = null; + } + + // Wait a bit before restarting + await Task.Delay(1000); + + // Rebuild the arguments (clear any previous appended udid) + string baseArgs = "--max-size 1080 --window-title=\"scrcpy_embed\" --stay-awake --disable-screensaver --window-x=-2000 --window-y=-2000"; + ScrcpyArguments = baseArgs + " -s " + currentUdid; + + // Start new scrcpy process + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = ScrcpyPath, + Arguments = ScrcpyArguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + scrcpyProcess = new Process { StartInfo = psi }; + bool started = scrcpyProcess.Start(); + + if (!started) + { + Logger.Error("Failed to restart scrcpy process"); + isRestarting = false; + processMonitorTimer?.Start(); + return; + } + + // Wait for scrcpy to initialize and create its window + IntPtr scrcpyHandle = IntPtr.Zero; + for (int attempts = 0; attempts < 50; attempts++) + { + await Task.Delay(200); + + if (scrcpyProcess.HasExited) + { + Logger.Error($"Scrcpy exited during restart with code: {scrcpyProcess.ExitCode}"); + isRestarting = false; + processMonitorTimer?.Start(); + return; + } + + scrcpyHandle = FindScrcpyWindow(); + if (scrcpyHandle != IntPtr.Zero) + break; + } + + if (scrcpyHandle == IntPtr.Zero) + { + Logger.Error("Could not find scrcpy window after restart"); + isRestarting = false; + processMonitorTimer?.Start(); + return; + } + + // Re-embed the window + EmbedWindow(scrcpyHandle); + + Logger.Info("Scrcpy restarted successfully"); + + // Reset restart attempts on successful restart + restartAttempts = 0; + + // Restart monitoring + processMonitorTimer?.Start(); + } + catch (Exception ex) + { + Logger.Error(ex, "Error during scrcpy restart"); + processMonitorTimer?.Start(); + } + finally + { + isRestarting = false; + } + } + + /// + /// Resets the restart attempt counter (useful after successful manual operations) + /// + public void ResetRestartCounter() + { + restartAttempts = 0; + Logger.Info("Restart counter reset"); + } /// /// Stops the scrcpy process and cleans up. @@ -184,14 +386,30 @@ public void Stop() { try { + // Stop the monitoring timer first + if (processMonitorTimer != null) + { + processMonitorTimer.Stop(); + processMonitorTimer.Dispose(); + processMonitorTimer = null; + Logger.Info("Process monitoring stopped"); + } + if (scrcpyProcess != null && !scrcpyProcess.HasExited) { scrcpyProcess.Kill(); scrcpyProcess.Dispose(); scrcpyProcess = null; + Logger.Info("Scrcpy process stopped"); } + + // Reset restart counter when stopping + restartAttempts = 0; + } + catch (Exception ex) + { + Logger.Error(ex, "Error stopping scrcpy"); } - catch { } } private async Task ResizeParentFormToScrcpyWindow(IntPtr scrcpyHandle) @@ -376,6 +594,13 @@ private void ShowError(string message) public void Dispose() { Stop(); + + if (processMonitorTimer != null) + { + processMonitorTimer.Dispose(); + processMonitorTimer = null; + } + if (hostPanel != null) { hostPanel.Dispose(); From 47ddc578aac5d514fe2a0a27dcc0e09c5989b03a Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sat, 11 Apr 2026 23:30:49 +0530 Subject: [PATCH 04/12] Play recorded steps for repeated times and duration --- Appium Wizard/ScreenControl.Designer.cs | 28 +++- Appium Wizard/ScreenControl.cs | 189 ++++++++++++++++++------ 2 files changed, 170 insertions(+), 47 deletions(-) diff --git a/Appium Wizard/ScreenControl.Designer.cs b/Appium Wizard/ScreenControl.Designer.cs index 91d9c21..2fce97c 100644 --- a/Appium Wizard/ScreenControl.Designer.cs +++ b/Appium Wizard/ScreenControl.Designer.cs @@ -58,6 +58,8 @@ private void InitializeComponent() objectSpyButton = new ToolStripButton(); RecordAndStopRecordingSteps = new ToolStripSplitButton(); playStepsToolStripMenuItem = new ToolStripMenuItem(); + playStepsWithRepetitionsToolStripMenuItem = new ToolStripMenuItem(); + playStepsWithDurationToolStripMenuItem = new ToolStripMenuItem(); readMeToolStripMenuItem = new ToolStripMenuItem(); TakeScreenshotToolStripSplitButton1 = new ToolStripSplitButton(); recordScreenToolStripMenuItem = new ToolStripMenuItem(); @@ -326,21 +328,37 @@ private void InitializeComponent() // RecordAndStopRecordingSteps.Alignment = ToolStripItemAlignment.Right; RecordAndStopRecordingSteps.DisplayStyle = ToolStripItemDisplayStyle.Image; - RecordAndStopRecordingSteps.DropDownItems.AddRange(new ToolStripItem[] { playStepsToolStripMenuItem, readMeToolStripMenuItem }); + RecordAndStopRecordingSteps.DropDownItems.AddRange(new ToolStripItem[] { playStepsToolStripMenuItem, playStepsWithRepetitionsToolStripMenuItem, playStepsWithDurationToolStripMenuItem, readMeToolStripMenuItem }); RecordAndStopRecordingSteps.Image = Properties.Resources.RecordSteps; RecordAndStopRecordingSteps.ImageTransparentColor = Color.Magenta; RecordAndStopRecordingSteps.Name = "RecordAndStopRecordingSteps"; RecordAndStopRecordingSteps.Size = new Size(40, 28); RecordAndStopRecordingSteps.ToolTipText = "Record and Playback"; RecordAndStopRecordingSteps.ButtonClick += RecordAndStopRecordingSteps_ButtonClick; - // + // // playStepsToolStripMenuItem - // + // playStepsToolStripMenuItem.Image = Properties.Resources.play; playStepsToolStripMenuItem.Name = "playStepsToolStripMenuItem"; - playStepsToolStripMenuItem.Size = new Size(127, 22); + playStepsToolStripMenuItem.Size = new Size(220, 22); playStepsToolStripMenuItem.Text = "Play Steps"; playStepsToolStripMenuItem.Click += playStepsToolStripMenuItem_Click; + // + // playStepsWithRepetitionsToolStripMenuItem + // + playStepsWithRepetitionsToolStripMenuItem.Image = Properties.Resources.play; + playStepsWithRepetitionsToolStripMenuItem.Name = "playStepsWithRepetitionsToolStripMenuItem"; + playStepsWithRepetitionsToolStripMenuItem.Size = new Size(220, 22); + playStepsWithRepetitionsToolStripMenuItem.Text = "Play Steps (Repetitions)"; + playStepsWithRepetitionsToolStripMenuItem.Click += playStepsWithRepetitionsToolStripMenuItem_Click; + // + // playStepsWithDurationToolStripMenuItem + // + playStepsWithDurationToolStripMenuItem.Image = Properties.Resources.play; + playStepsWithDurationToolStripMenuItem.Name = "playStepsWithDurationToolStripMenuItem"; + playStepsWithDurationToolStripMenuItem.Size = new Size(220, 22); + playStepsWithDurationToolStripMenuItem.Text = "Play Steps (Duration)"; + playStepsWithDurationToolStripMenuItem.Click += playStepsWithDurationToolStripMenuItem_Click; // // readMeToolStripMenuItem // @@ -447,6 +465,8 @@ private void InitializeComponent() private ToolStripStatusLabel toolStripStatusLabel; private ToolStripSplitButton RecordAndStopRecordingSteps; private ToolStripMenuItem playStepsToolStripMenuItem; + private ToolStripMenuItem playStepsWithRepetitionsToolStripMenuItem; + private ToolStripMenuItem playStepsWithDurationToolStripMenuItem; private ToolStripMenuItem readMeToolStripMenuItem; private ToolStripMenuItem infoToolStripMenuItem; private ToolStripMenuItem copyProxyPortToolStripMenuItem1; diff --git a/Appium Wizard/ScreenControl.cs b/Appium Wizard/ScreenControl.cs index 7be7731..2566629 100644 --- a/Appium Wizard/ScreenControl.cs +++ b/Appium Wizard/ScreenControl.cs @@ -1,5 +1,6 @@ using Appium_Wizard.Appium_Wizard; using Appium_Wizard.Properties; +using Microsoft.VisualBasic; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; using NLog; @@ -1274,57 +1275,159 @@ private async void playStepsToolStripMenuItem_Click(object sender, EventArgs e) playStepsToolStripMenuItem.Enabled = false; await Task.Run(() => { - foreach (var action in recordedActions) + ExecuteRecordedActions(); + }); + MessageBox.Show("Steps playback completed.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); + GoogleAnalytics.SendEvent("Steps playback completed", OSType); + playStepsToolStripMenuItem.Enabled = true; + } + + private async void playStepsWithRepetitionsToolStripMenuItem_Click(object sender, EventArgs e) + { + if (isRecordingSteps) + { + MessageBox.Show("Recording is in progress. Please stop the recording and then play.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (recordedActions.Count <= 1) + { + MessageBox.Show("No steps recorded to play.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Prompt user for number of repetitions using InputBox + string input = Interaction.InputBox("Enter the number of times to repeat the steps:", "Play Steps - Repetitions", "1"); + + if (string.IsNullOrWhiteSpace(input)) + { + return; // User cancelled + } + + if (!int.TryParse(input, out int repetitions) || repetitions <= 0) + { + MessageBox.Show("Please enter a valid positive number.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + playStepsToolStripMenuItem.Enabled = false; + playStepsWithRepetitionsToolStripMenuItem.Enabled = false; + playStepsWithDurationToolStripMenuItem.Enabled = false; + + await Task.Run(() => + { + for (int i = 0; i < repetitions; i++) { - switch (action.ActionType) - { - case "Click on coordinates": - if (isAndroid) - { - AndroidMethods.GetInstance().Tap(udid, action.X, action.Y); - } - else - { - iOSAPIMethods.Tap(URL, sessionId, action.X, action.Y); - } - break; + ExecuteRecordedActions(); + } + }); - case "Send Text Without Element": - if (isAndroid) - { - AndroidMethods.GetInstance().SendText(udid, action.Text); - } - else - { - iOSAPIMethods.SendText(URL, sessionId, action.Text); - } - break; + MessageBox.Show($"Steps playback completed ({repetitions} repetition(s)).", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); + GoogleAnalytics.SendEvent($"Steps playback completed with {repetitions} repetitions", OSType); - case "Home": - if (isAndroid) - { - AndroidMethods.GetInstance().GoToHome(udid); - } - else - { - iOSAPIMethods.GoToHome(proxyPort); - } - break; + playStepsToolStripMenuItem.Enabled = true; + playStepsWithRepetitionsToolStripMenuItem.Enabled = true; + playStepsWithDurationToolStripMenuItem.Enabled = true; + } - case "Back": - if (isAndroid) - { - AndroidMethods.GetInstance().Back(udid); - } - break; - } + private async void playStepsWithDurationToolStripMenuItem_Click(object sender, EventArgs e) + { + if (isRecordingSteps) + { + MessageBox.Show("Recording is in progress. Please stop the recording and then play.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (recordedActions.Count <= 1) + { + MessageBox.Show("No steps recorded to play.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // Prompt user for duration in seconds using InputBox + string input = Interaction.InputBox("Enter the duration in seconds to repeat the steps:", "Play Steps - Duration", "60"); + + if (string.IsNullOrWhiteSpace(input)) + { + return; // User cancelled + } + + if (!int.TryParse(input, out int durationSeconds) || durationSeconds <= 0) + { + MessageBox.Show("Please enter a valid positive number of seconds.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + playStepsToolStripMenuItem.Enabled = false; + playStepsWithRepetitionsToolStripMenuItem.Enabled = false; + playStepsWithDurationToolStripMenuItem.Enabled = false; + + int executionCount = 0; + await Task.Run(() => + { + var startTime = DateTime.Now; + var endTime = startTime.AddSeconds(durationSeconds); - //await Task.Delay(500); // Add delay between actions + while (DateTime.Now < endTime) + { + ExecuteRecordedActions(); + executionCount++; } }); - MessageBox.Show("Steps playback completed.", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); - GoogleAnalytics.SendEvent("Steps playback completed", OSType); + + MessageBox.Show($"Steps playback completed ({executionCount} execution(s) in {durationSeconds} seconds).", "Play Steps", MessageBoxButtons.OK, MessageBoxIcon.Information); + GoogleAnalytics.SendEvent($"Steps playback completed with {durationSeconds}s duration", OSType); + playStepsToolStripMenuItem.Enabled = true; + playStepsWithRepetitionsToolStripMenuItem.Enabled = true; + playStepsWithDurationToolStripMenuItem.Enabled = true; + } + + private void ExecuteRecordedActions() + { + foreach (var action in recordedActions) + { + switch (action.ActionType) + { + case "Click on coordinates": + if (isAndroid) + { + AndroidMethods.GetInstance().Tap(udid, action.X, action.Y); + } + else + { + iOSAPIMethods.Tap(URL, sessionId, action.X, action.Y); + } + break; + + case "Send Text Without Element": + if (isAndroid) + { + AndroidMethods.GetInstance().SendText(udid, action.Text); + } + else + { + iOSAPIMethods.SendText(URL, sessionId, action.Text); + } + break; + + case "Home": + if (isAndroid) + { + AndroidMethods.GetInstance().GoToHome(udid); + } + else + { + iOSAPIMethods.GoToHome(proxyPort); + } + break; + + case "Back": + if (isAndroid) + { + AndroidMethods.GetInstance().Back(udid); + } + break; + } + } } private void RecordAndStopRecordingSteps_ButtonClick(object sender, EventArgs e) From 77b1f2eecbace3d741df1474cb7b77f209557a28 Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sat, 11 Apr 2026 23:58:22 +0530 Subject: [PATCH 05/12] Open CMD in adb and ios path to run custom commands --- .../ExcludeInInstaller/Installer Script.iss | 2 +- Appium Wizard/MainScreen.Designer.cs | 37 +++++++++++++- Appium Wizard/MainScreen.cs | 50 +++++++++++++++++++ Appium Wizard/VersionInfo.cs | 4 +- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/Appium Wizard/ExcludeInInstaller/Installer Script.iss b/Appium Wizard/ExcludeInInstaller/Installer Script.iss index 042459c..8279212 100644 --- a/Appium Wizard/ExcludeInInstaller/Installer Script.iss +++ b/Appium Wizard/ExcludeInInstaller/Installer Script.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Appium Wizard" -#define MyAppVersion "8.6.1" +#define MyAppVersion "9.0.0" #define MyAppPublisher "Meganathan C" #define MyAppExeName "Appium Wizard.exe" diff --git a/Appium Wizard/MainScreen.Designer.cs b/Appium Wizard/MainScreen.Designer.cs index e55cc34..e7ba3e1 100644 --- a/Appium Wizard/MainScreen.Designer.cs +++ b/Appium Wizard/MainScreen.Designer.cs @@ -63,6 +63,9 @@ private void InitializeComponent() signIPAToolStripMenuItem = new ToolStripMenuItem(); testRunnerToolStripMenuItem = new ToolStripMenuItem(); downloadWDAToolStripMenuItem = new ToolStripMenuItem(); + openCMDToolStripMenuItem = new ToolStripMenuItem(); + adbCommandsToolStripMenuItem = new ToolStripMenuItem(); + iOSCommandsToolStripMenuItem = new ToolStripMenuItem(); settingsToolStripMenuItem = new ToolStripMenuItem(); iOSProxyToolStripMenuItem = new ToolStripMenuItem(); iOSExecutorToolStripMenuItem = new ToolStripMenuItem(); @@ -365,9 +368,9 @@ private void InitializeComponent() // // toolsToolStripMenuItem // - toolsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { inspectorToolStripMenuItem1, iOSProfileManagementToolStripMenuItem, signIPAToolStripMenuItem, testRunnerToolStripMenuItem, downloadWDAToolStripMenuItem }); + toolsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { inspectorToolStripMenuItem1, iOSProfileManagementToolStripMenuItem, signIPAToolStripMenuItem, testRunnerToolStripMenuItem, downloadWDAToolStripMenuItem, openCMDToolStripMenuItem }); toolsToolStripMenuItem.Name = "toolsToolStripMenuItem"; - toolsToolStripMenuItem.Size = new Size(46, 20); + toolsToolStripMenuItem.Size = new Size(47, 20); toolsToolStripMenuItem.Text = "Tools"; // // inspectorToolStripMenuItem1 @@ -411,6 +414,33 @@ private void InitializeComponent() downloadWDAToolStripMenuItem.ToolTipText = "Downloads the WebDriverAgentRunner IPA file from official Github repository."; downloadWDAToolStripMenuItem.Click += downloadWDAToolStripMenuItem_Click; // + // openCMDToolStripMenuItem + // + openCMDToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { adbCommandsToolStripMenuItem, iOSCommandsToolStripMenuItem }); + openCMDToolStripMenuItem.Image = Properties.Resources.terminal; + openCMDToolStripMenuItem.Name = "openCMDToolStripMenuItem"; + openCMDToolStripMenuItem.Size = new Size(211, 30); + openCMDToolStripMenuItem.Text = "Open CMD"; + openCMDToolStripMenuItem.ToolTipText = "Opens command prompt with ADB or iOS server commands."; + // + // adbCommandsToolStripMenuItem + // + adbCommandsToolStripMenuItem.Image = Properties.Resources.android; + adbCommandsToolStripMenuItem.Name = "adbCommandsToolStripMenuItem"; + adbCommandsToolStripMenuItem.Size = new Size(188, 30); + adbCommandsToolStripMenuItem.Text = "ADB Commands"; + adbCommandsToolStripMenuItem.ToolTipText = "Opens CMD in ADB path and displays available ADB commands."; + adbCommandsToolStripMenuItem.Click += adbCommandsToolStripMenuItem_Click; + // + // iOSCommandsToolStripMenuItem + // + iOSCommandsToolStripMenuItem.Image = Properties.Resources.apple; + iOSCommandsToolStripMenuItem.Name = "iOSCommandsToolStripMenuItem"; + iOSCommandsToolStripMenuItem.Size = new Size(188, 30); + iOSCommandsToolStripMenuItem.Text = "iOS Commands"; + iOSCommandsToolStripMenuItem.ToolTipText = "Opens CMD in iOS server path and displays available iOS server commands."; + iOSCommandsToolStripMenuItem.Click += iOSCommandsToolStripMenuItem_Click; + // // settingsToolStripMenuItem // settingsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { iOSProxyToolStripMenuItem, iOSExecutorToolStripMenuItem, notificationsToolStripMenuItem, androidScreenMirroringToolStripMenuItem, alwaysOnTopToolStripMenuItem }); @@ -1224,5 +1254,8 @@ private void InitializeComponent() private ToolStripMenuItem androidScreenMirroringToolStripMenuItem; private ToolStripMenuItem useScrcpyMenuItem; private ToolStripMenuItem useUiAutomator2ToolStripMenuItem; + private ToolStripMenuItem openCMDToolStripMenuItem; + private ToolStripMenuItem adbCommandsToolStripMenuItem; + private ToolStripMenuItem iOSCommandsToolStripMenuItem; } } \ No newline at end of file diff --git a/Appium Wizard/MainScreen.cs b/Appium Wizard/MainScreen.cs index b74f461..8c53492 100644 --- a/Appium Wizard/MainScreen.cs +++ b/Appium Wizard/MainScreen.cs @@ -2312,6 +2312,56 @@ private void downloadWDAToolStripMenuItem_Click(object sender, EventArgs e) } } + private void adbCommandsToolStripMenuItem_Click(object sender, EventArgs e) + { + try + { + string adbPath = Path.GetDirectoryName(FilesPath.adbFilePath); + string windowTitle = "ADB Commands - Appium Wizard"; + string cmdArguments = $"/K \"title {windowTitle} && cd /d \"{adbPath}\" && adb\""; + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = cmdArguments, + UseShellExecute = false, + CreateNoWindow = false, + WorkingDirectory = adbPath + }; + Process.Start(psi); + GoogleAnalytics.SendEvent("adbCommandsToolStripMenuItem_Click"); + } + catch (Exception exception) + { + GoogleAnalytics.SendExceptionEvent("adbCommandsToolStripMenuItem_Click", exception.Message); + } + } + + private void iOSCommandsToolStripMenuItem_Click(object sender, EventArgs e) + { + try + { + string iOSServerPath = Path.GetDirectoryName(FilesPath.iOSServerFilePath); + string windowTitle = "iOS Commands - Appium Wizard"; + string cmdArguments = $"/K \"title {windowTitle} && cd /d \"{iOSServerPath}\" && doskey ios=iOSServer.exe $* && iOSServer.exe\""; + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = cmdArguments, + UseShellExecute = false, + CreateNoWindow = false, + WorkingDirectory = iOSServerPath + }; + Process.Start(psi); + GoogleAnalytics.SendEvent("iOSCommandsToolStripMenuItem_Click"); + } + catch (Exception exception) + { + GoogleAnalytics.SendExceptionEvent("iOSCommandsToolStripMenuItem_Click", exception.Message); + } + } + private void openLogsButton_Click(object sender, EventArgs e) { try diff --git a/Appium Wizard/VersionInfo.cs b/Appium Wizard/VersionInfo.cs index c3b1140..326a591 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.1"; - public const string ReleaseNotes = "Some iPhones not working fix"; + public const string VersionNumber = "9.0.0"; + public const string ReleaseNotes = "Execution speed issue fix"; } } From a7458efaa4aa7b85c58459842b494db730c4e7db Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 14:19:58 +0530 Subject: [PATCH 06/12] if status text and drawing disabled dont process elements --- Appium Wizard/ExecutionStatus.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Appium Wizard/ExecutionStatus.cs b/Appium Wizard/ExecutionStatus.cs index 7879c03..2c0fe4b 100644 --- a/Appium Wizard/ExecutionStatus.cs +++ b/Appium Wizard/ExecutionStatus.cs @@ -23,6 +23,13 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) { this.screenControl = screenControl; screenDensity = screenControl.screenDensity; + + // Early exit if both status text and drawing are disabled - no need to process anything + if (!screenControl.ShowExecutionStatusText && !screenControl.ShowExecutionDrawing) + { + return; + } + try { string statusText; @@ -278,7 +285,11 @@ public void UpdateScreenControl(ScreenControl screenControl, string data) if (!string.IsNullOrEmpty(elementId) && !string.IsNullOrEmpty(url)) { - GetRectValues(url, elementId); + // Only fetch rect values if drawing is enabled (avoids expensive HTTP calls to UIAutomator2/WDA) + if (screenControl.ShowExecutionDrawing) + { + GetRectValues(url, elementId); + } } } } From 6d5d3e986dde6f41898d2e05743f22f7bca03dcb Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 15:09:32 +0530 Subject: [PATCH 07/12] object spy improvements --- Appium Wizard/Object Spy.Designer.cs | 72 +++++++++++++++++++++++----- Appium Wizard/Object Spy.cs | 54 +++++++++++++++++++++ 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/Appium Wizard/Object Spy.Designer.cs b/Appium Wizard/Object Spy.Designer.cs index 6d636d7..77f23b7 100644 --- a/Appium Wizard/Object Spy.Designer.cs +++ b/Appium Wizard/Object Spy.Designer.cs @@ -45,6 +45,8 @@ private void InitializeComponent() listViewContextMenuStrip = new ContextMenuStrip(components); copyXpathToolStripMenuItem = new ToolStripMenuItem(); addToFilterToolStripMenuItem = new ToolStripMenuItem(); + copyKeyToolStripMenuItem = new ToolStripMenuItem(); + copyValueToolStripMenuItem = new ToolStripMenuItem(); treeViewContextMenuStrip = new ContextMenuStrip(components); copyUniqueXpathToolStripMenuItem = new ToolStripMenuItem(); addUniqueXpathToFilterToolStripMenuItem = new ToolStripMenuItem(); @@ -55,6 +57,8 @@ private void InitializeComponent() coordLabel = new Label(); helpButton = new Button(); downloadXmlButton = new Button(); + copyXmlButton = new Button(); + expandCollapseButton = new Button(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); listViewContextMenuStrip.SuspendLayout(); treeViewContextMenuStrip.SuspendLayout(); @@ -177,27 +181,41 @@ private void InitializeComponent() nextButton.Text = ">"; nextButton.UseVisualStyleBackColor = true; nextButton.Click += nextButton_Click; - // + // // listViewContextMenuStrip - // - listViewContextMenuStrip.Items.AddRange(new ToolStripItem[] { copyXpathToolStripMenuItem, addToFilterToolStripMenuItem }); + // + listViewContextMenuStrip.Items.AddRange(new ToolStripItem[] { copyXpathToolStripMenuItem, addToFilterToolStripMenuItem, copyKeyToolStripMenuItem, copyValueToolStripMenuItem }); listViewContextMenuStrip.Name = "contextMenuStrip1"; - listViewContextMenuStrip.Size = new Size(140, 48); - // + listViewContextMenuStrip.Size = new Size(180, 92); + // // copyXpathToolStripMenuItem - // + // copyXpathToolStripMenuItem.Name = "copyXpathToolStripMenuItem"; - copyXpathToolStripMenuItem.Size = new Size(139, 22); + copyXpathToolStripMenuItem.Size = new Size(179, 22); copyXpathToolStripMenuItem.Text = "Copy XPath"; copyXpathToolStripMenuItem.Click += copyXpathToolStripMenuItem_Click; - // + // // addToFilterToolStripMenuItem - // + // addToFilterToolStripMenuItem.Name = "addToFilterToolStripMenuItem"; - addToFilterToolStripMenuItem.Size = new Size(139, 22); + addToFilterToolStripMenuItem.Size = new Size(179, 22); addToFilterToolStripMenuItem.Text = "Add to Filter"; addToFilterToolStripMenuItem.Click += addToFilterToolStripMenuItem_Click; - // + // + // copyKeyToolStripMenuItem + // + copyKeyToolStripMenuItem.Name = "copyKeyToolStripMenuItem"; + copyKeyToolStripMenuItem.Size = new Size(179, 22); + copyKeyToolStripMenuItem.Text = "Copy Key"; + copyKeyToolStripMenuItem.Click += copyKeyToolStripMenuItem_Click; + // + // copyValueToolStripMenuItem + // + copyValueToolStripMenuItem.Name = "copyValueToolStripMenuItem"; + copyValueToolStripMenuItem.Size = new Size(179, 22); + copyValueToolStripMenuItem.Text = "Copy Value"; + copyValueToolStripMenuItem.Click += copyValueToolStripMenuItem_Click; + // // treeViewContextMenuStrip // treeViewContextMenuStrip.Items.AddRange(new ToolStripItem[] { copyUniqueXpathToolStripMenuItem, addUniqueXpathToFilterToolStripMenuItem }); @@ -271,12 +289,34 @@ private void InitializeComponent() downloadXmlButton.AutoSize = true; downloadXmlButton.Location = new Point(475, 8); downloadXmlButton.Name = "downloadXmlButton"; - downloadXmlButton.Size = new Size(98, 25); + downloadXmlButton.Size = new Size(110, 25); downloadXmlButton.TabIndex = 16; downloadXmlButton.Text = "Download XML"; downloadXmlButton.UseVisualStyleBackColor = true; downloadXmlButton.Click += downloadXmlButton_Click; - // + // + // copyXmlButton + // + copyXmlButton.AutoSize = true; + copyXmlButton.Location = new Point(600, 8); + copyXmlButton.Name = "copyXmlButton"; + copyXmlButton.Size = new Size(84, 25); + copyXmlButton.TabIndex = 17; + copyXmlButton.Text = "Copy XML"; + copyXmlButton.UseVisualStyleBackColor = true; + copyXmlButton.Click += copyXmlButton_Click; + // + // expandCollapseButton + // + expandCollapseButton.AutoSize = true; + expandCollapseButton.Location = new Point(700, 8); + expandCollapseButton.Name = "expandCollapseButton"; + expandCollapseButton.Size = new Size(84, 25); + expandCollapseButton.TabIndex = 18; + expandCollapseButton.Text = "Expand All"; + expandCollapseButton.UseVisualStyleBackColor = true; + expandCollapseButton.Click += expandCollapseButton_Click; + // // Object_Spy // AutoScaleDimensions = new SizeF(7F, 15F); @@ -285,6 +325,8 @@ private void InitializeComponent() AutoSizeMode = AutoSizeMode.GrowAndShrink; BackColor = SystemColors.ControlLightLight; ClientSize = new Size(1214, 592); + Controls.Add(expandCollapseButton); + Controls.Add(copyXmlButton); Controls.Add(downloadXmlButton); Controls.Add(helpButton); Controls.Add(coordLabel); @@ -332,6 +374,8 @@ private void InitializeComponent() private ContextMenuStrip listViewContextMenuStrip; private ToolStripMenuItem addToFilterToolStripMenuItem; private ToolStripMenuItem copyXpathToolStripMenuItem; + private ToolStripMenuItem copyKeyToolStripMenuItem; + private ToolStripMenuItem copyValueToolStripMenuItem; private ContextMenuStrip treeViewContextMenuStrip; private ToolStripMenuItem copyUniqueXpathToolStripMenuItem; private ToolStripMenuItem addUniqueXpathToFilterToolStripMenuItem; @@ -342,5 +386,7 @@ private void InitializeComponent() private Label coordLabel; private Button helpButton; private Button downloadXmlButton; + private Button copyXmlButton; + private Button expandCollapseButton; } } \ No newline at end of file diff --git a/Appium Wizard/Object Spy.cs b/Appium Wizard/Object Spy.cs index 4138f2e..a786cbb 100644 --- a/Appium Wizard/Object Spy.cs +++ b/Appium Wizard/Object Spy.cs @@ -1242,6 +1242,60 @@ private void downloadXmlButton_Click(object sender, EventArgs e) } } + private void copyXmlButton_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(xmlContent)) + { + MessageBox.Show("No XML content available to copy. Please refresh the screen first.", "No Content", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + Clipboard.SetText(xmlContent); + MessageBox.Show("XML copied to clipboard successfully!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + Logger.Error(ex, "Error copying XML to clipboard"); + MessageBox.Show($"Error copying XML to clipboard:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void copyKeyToolStripMenuItem_Click(object sender, EventArgs e) + { + if (listView1.SelectedItems.Count > 0) + { + ListViewItem selectedItem = listView1.SelectedItems[0]; + string property = selectedItem.Text; + Clipboard.SetText(property); + } + } + + private void copyValueToolStripMenuItem_Click(object sender, EventArgs e) + { + if (listView1.SelectedItems.Count > 0) + { + ListViewItem selectedItem = listView1.SelectedItems[0]; + string value = selectedItem.SubItems[1].Text; + Clipboard.SetText(value); + } + } + + private void expandCollapseButton_Click(object sender, EventArgs e) + { + if (expandCollapseButton.Text == "Expand All") + { + treeView1.ExpandAll(); + expandCollapseButton.Text = "Collapse All"; + } + else + { + treeView1.CollapseAll(); + expandCollapseButton.Text = "Expand All"; + } + } + private void Object_Spy_FormClosing(object sender, FormClosingEventArgs e) { if (isAndroid) From 3abd2a5f237fd79cf92df6ad3d2a5b79e284128e Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 15:18:40 +0530 Subject: [PATCH 08/12] free text search added in object spy --- Appium Wizard/Object Spy.Designer.cs | 24 ++++++++--- Appium Wizard/Object Spy.cs | 62 +++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/Appium Wizard/Object Spy.Designer.cs b/Appium Wizard/Object Spy.Designer.cs index 77f23b7..7cc0985 100644 --- a/Appium Wizard/Object Spy.Designer.cs +++ b/Appium Wizard/Object Spy.Designer.cs @@ -36,6 +36,7 @@ private void InitializeComponent() Property = new ColumnHeader(); Value = new ColumnHeader(); filterTextbox = new RichTextBox(); + searchModeComboBox = new ComboBox(); refreshButton = new Button(); elementNumberTextbox = new TextBox(); label1 = new Label(); @@ -111,10 +112,21 @@ private void InitializeComponent() // Value.Text = "Value"; Value.Width = 500; - // + // + // searchModeComboBox + // + searchModeComboBox.DropDownStyle = ComboBoxStyle.DropDownList; + searchModeComboBox.FormattingEnabled = true; + searchModeComboBox.Items.AddRange(new object[] { "XPath", "Free Text" }); + searchModeComboBox.Location = new Point(361, 508); + searchModeComboBox.Name = "searchModeComboBox"; + searchModeComboBox.Size = new Size(100, 23); + searchModeComboBox.TabIndex = 3; + searchModeComboBox.SelectedIndexChanged += searchModeComboBox_SelectedIndexChanged; + // // filterTextbox - // - filterTextbox.Location = new Point(361, 524); + // + filterTextbox.Location = new Point(361, 537); filterTextbox.Name = "filterTextbox"; filterTextbox.Size = new Size(642, 50); filterTextbox.TabIndex = 4; @@ -237,10 +249,10 @@ private void InitializeComponent() addUniqueXpathToFilterToolStripMenuItem.Click += addUniqueXpathToFilterToolStripMenuItem_Click; // // filterLabel - // + // filterLabel.AutoSize = true; filterLabel.Font = new Font("Segoe UI", 9F, FontStyle.Bold); - filterLabel.Location = new Point(359, 508); + filterLabel.Location = new Point(467, 511); filterLabel.Name = "filterLabel"; filterLabel.Size = new Size(132, 15); filterLabel.TabIndex = 13; @@ -337,6 +349,7 @@ private void InitializeComponent() Controls.Add(label1); Controls.Add(elementNumberTextbox); Controls.Add(refreshButton); + Controls.Add(searchModeComboBox); Controls.Add(filterTextbox); Controls.Add(listView1); Controls.Add(treeView1); @@ -365,6 +378,7 @@ private void InitializeComponent() private ColumnHeader Property; private ColumnHeader Value; private RichTextBox filterTextbox; + private ComboBox searchModeComboBox; private Button refreshButton; private TextBox elementNumberTextbox; private Label label1; diff --git a/Appium Wizard/Object Spy.cs b/Appium Wizard/Object Spy.cs index a786cbb..34ca20e 100644 --- a/Appium Wizard/Object Spy.cs +++ b/Appium Wizard/Object Spy.cs @@ -38,6 +38,9 @@ public Object_Spy(string os, int port, int width, int height, string sessionId, private async void Object_Spy_Load(object sender, EventArgs e) { + // Initialize search mode to XPath + searchModeComboBox.SelectedIndex = 0; // XPath + if (isAndroid) { string messageTitle = "Object Spy - " + deviceName; @@ -650,6 +653,22 @@ private async void refreshButton_Click(object sender, EventArgs e) private List matchingNodes = new List(); private int currentIndex = -1; + private void searchModeComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + // Update the label text based on selected mode + if (searchModeComboBox.SelectedIndex == 0) // XPath + { + filterLabel.Text = "Filter (XPath Validator)"; + } + else // Free Text + { + filterLabel.Text = "Filter (Free Text Search)"; + } + + // Re-run the filter with the current text + xpathTextbox_TextChanged(sender, e); + } + private void xpathTextbox_TextChanged(object sender, EventArgs e) { try @@ -670,17 +689,41 @@ private void xpathTextbox_TextChanged(object sender, EventArgs e) elementNumberTextbox.Text = "0"; return; } + XmlNodeList selectedXmlNodes; - try + + // Check the search mode + if (searchModeComboBox.SelectedIndex == 0) // XPath mode { - selectedXmlNodes = xmlDoc.SelectNodes(filterText); + try + { + selectedXmlNodes = xmlDoc.SelectNodes(filterText); + } + catch (Exception ex) + { + filterText = filterText.Replace("='", "=\"").Replace("']", "\"]").Replace("['", "[\"").Replace("' and ", "\" and "); + selectedXmlNodes = xmlDoc.SelectNodes(filterText); + } } - catch (Exception ex) + else // Free Text mode { - filterText = filterText.Replace("='", "=\"").Replace("']", "\"]").Replace("['", "[\"").Replace("' and ", "\" and "); - selectedXmlNodes = xmlDoc.SelectNodes(filterText); - } + // Search for text in common text attributes + // Build XPath to search in text, content-desc, name, value, and label attributes + string escapedText = filterText.Replace("'", "'"); + string freeTextXPath = $"//*[contains(@text, '{escapedText}') or contains(@content-desc, '{escapedText}') or contains(@name, '{escapedText}') or contains(@value, '{escapedText}') or contains(@label, '{escapedText}')]"; + try + { + selectedXmlNodes = xmlDoc.SelectNodes(freeTextXPath); + } + catch (Exception) + { + filterTextbox.ForeColor = Color.Red; + TotalElementCount.Text = "0"; + elementNumberTextbox.Text = "0"; + return; + } + } matchingNodes.Clear(); currentIndex = -1; @@ -914,6 +957,9 @@ private void copyXpathToolStripMenuItem_Click(object sender, EventArgs e) private void addToFilterToolStripMenuItem_Click(object sender, EventArgs e) { //GoogleAnalytics.SendEvent("Object_Spy_ListView_AddToFilter", os); + // Switch to XPath mode since we're adding XPath predicates + searchModeComboBox.SelectedIndex = 0; + if (listView1.SelectedItems.Count > 1) // multiple row selected { var predicates = new List(); @@ -1105,6 +1151,8 @@ private void addUniqueXpathToFilterToolStripMenuItem_Click(object sender, EventA //GoogleAnalytics.SendEvent("Object_Spy_TreeView_AddToFilter", os); if (treeView1.SelectedNode != null && treeView1.SelectedNode.Tag is XmlNode selectedXmlNode) { + // Switch to XPath mode since we're adding an XPath + searchModeComboBox.SelectedIndex = 0; string uniqueXPath = GenerateUniqueXPath(selectedXmlNode); filterTextbox.Text = uniqueXPath; treeView1.SelectedNode = treeView1.SelectedNode; @@ -1162,6 +1210,8 @@ private void addUniqueXpathToFilterToolStripMenuItem1_Click(object sender, Event { clickedElement = FindElementByCoordinates(clickedX, clickedY); } + // Switch to XPath mode since we're adding an XPath + searchModeComboBox.SelectedIndex = 0; string uniqueXPath = GenerateUniqueXPath(clickedElement); filterTextbox.Text = uniqueXPath; } From f32dca0cda8cd2c3045ca525a564cd8315d48ab9 Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 15:33:14 +0530 Subject: [PATCH 09/12] object spy some elements not finding fix --- Appium Wizard/Object Spy.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Appium Wizard/Object Spy.cs b/Appium Wizard/Object Spy.cs index 34ca20e..5465f21 100644 --- a/Appium Wizard/Object Spy.cs +++ b/Appium Wizard/Object Spy.cs @@ -228,7 +228,7 @@ private bool IsPointInElementBounds(XmlNode node, int x, int y) Console.WriteLine($"Checking Android node: {node.Name}, Bounds: [{elementX}, {elementY}, {x2}, {y2}], Point: ({x}, {y})"); // Validate bounds - if (elementX > 0 && elementY > 0 && elementHeight > 0 && elementHeight > 0) + if (elementX >= 0 && elementY >= 0 && elementWidth > 0 && elementHeight > 0) { return x >= elementX && x <= (elementX + elementWidth) && y >= elementY && y <= (elementY + elementHeight); } @@ -253,7 +253,7 @@ private bool IsPointInElementBounds(XmlNode node, int x, int y) Console.WriteLine($"Checking node: {node.Name}, Bounds: [{elementX}, {elementY}, {elementWidth}, {elementHeight}], Point: ({x}, {y})"); // Validate bounds - if (elementX > 0 && elementY > 0 && elementHeight > 0 && elementHeight > 0) + if (elementX >= 0 && elementY >= 0 && elementWidth > 0 && elementHeight > 0) { return x >= elementX && x <= (elementX + elementWidth) && y >= elementY && y <= (elementY + elementHeight); } @@ -707,10 +707,12 @@ private void xpathTextbox_TextChanged(object sender, EventArgs e) } else // Free Text mode { - // Search for text in common text attributes + // Search for text in common text attributes (case-insensitive) // Build XPath to search in text, content-desc, name, value, and label attributes - string escapedText = filterText.Replace("'", "'"); - string freeTextXPath = $"//*[contains(@text, '{escapedText}') or contains(@content-desc, '{escapedText}') or contains(@name, '{escapedText}') or contains(@value, '{escapedText}') or contains(@label, '{escapedText}')]"; + string escapedText = filterText.Replace("'", "'").ToLower(); + string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + string lower = "abcdefghijklmnopqrstuvwxyz"; + string freeTextXPath = $"//*[contains(translate(@text, '{upper}', '{lower}'), '{escapedText}') or contains(translate(@content-desc, '{upper}', '{lower}'), '{escapedText}') or contains(translate(@name, '{upper}', '{lower}'), '{escapedText}') or contains(translate(@value, '{upper}', '{lower}'), '{escapedText}') or contains(translate(@label, '{upper}', '{lower}'), '{escapedText}')]"; try { From 3945100f01e44a934f035989ca8fc53312e2b541 Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 16:05:59 +0530 Subject: [PATCH 10/12] Google analytics update --- Appium Wizard/DeviceInformation.cs | 7 ++ Appium Wizard/GoogleAnalytics.cs | 166 +++++++++++++++++++++++------ Appium Wizard/LoadingScreen.cs | 4 + Appium Wizard/MainScreen.cs | 2 +- 4 files changed, 147 insertions(+), 32 deletions(-) diff --git a/Appium Wizard/DeviceInformation.cs b/Appium Wizard/DeviceInformation.cs index 79b61f1..e014eb1 100644 --- a/Appium Wizard/DeviceInformation.cs +++ b/Appium Wizard/DeviceInformation.cs @@ -73,6 +73,13 @@ private void Add_Click(object sender, EventArgs e) Hide(); Database.InsertDataIntoDevicesTable(DeviceName.Replace("'", "''"), OSType, OSVersion, Model, "Online", udid, Width, Height, Connection, IPAddress); mainScreen.addToList(DeviceName, OSVersion, udid, OSType, Model, "Online", Connection, IPAddress); + + // Update user properties to track primary device (most recently added) + string deviceOsType = OSType.ToLower().Contains("ios") ? "iOS" : "Android"; + GoogleAnalytics.SetUserProperty("primary_device_os", deviceOsType); + GoogleAnalytics.SetUserProperty("primary_device_model", Model); + GoogleAnalytics.SetUserProperty("primary_device_os_version", OSVersion); + if (OSType.ToLower().Contains("ios")) { Dictionary dic = new Dictionary(); diff --git a/Appium Wizard/GoogleAnalytics.cs b/Appium Wizard/GoogleAnalytics.cs index a9f2664..6e9f2d0 100644 --- a/Appium Wizard/GoogleAnalytics.cs +++ b/Appium Wizard/GoogleAnalytics.cs @@ -9,6 +9,36 @@ public class GoogleAnalytics static string MeasurementID = "{GOOGLEANALYTICSMEASUREMENTID}"; public static string clientId = string.Empty; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static Dictionary userProperties = new Dictionary(); + + /// + /// Sets a user property that will be sent with all future events. + /// User properties are useful for segmenting users in GA4 dashboards. + /// + /// Name of the user property (e.g., "app_version", "os_type") + /// Value of the user property + public static void SetUserProperty(string propertyName, string propertyValue) + { + if (!string.IsNullOrEmpty(propertyName) && !string.IsNullOrEmpty(propertyValue)) + { + userProperties[propertyName] = new { value = propertyValue }; + Logger.Info($"User property set: {propertyName} = {propertyValue}"); + } + } + + /// + /// Sets multiple user properties at once. + /// + public static void SetUserProperties(Dictionary properties) + { + if (properties != null) + { + foreach (var kvp in properties) + { + SetUserProperty(kvp.Key, kvp.Value); + } + } + } public static void SendEvent(object eventName, string info = "", bool addUserCount = false) { @@ -21,9 +51,31 @@ public static void SendEvent(object eventName, string info = "", bool addUserCou { var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://www.google-analytics.com/mp/collect?api_secret=" + APISecret + "&measurement_id=" + MeasurementID); - StringContent? content = null; - int userCount = addUserCount ? 1 : 0; - content = new StringContent($"{{\"client_id\":\"{clientId}\",\"non_personalized_ads\":true,\"events\":[{{\"name\":\"{eventName}\",\"params\":{{\"engagement_time_msec\":{userCount},\"Info\":\"{info}\"}}}}]}}", null, "application/json"); + + var payload = new + { + client_id = clientId, + non_personalized_ads = true, + user_properties = userProperties.Count > 0 ? userProperties : null, + events = new[] + { + new + { + name = eventName.ToString(), + @params = new Dictionary + { + { "Info", info } + } + } + } + }; + + string jsonPayload = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + + var content = new StringContent(jsonPayload, null, "application/json"); request.Content = content; client.Send(request); } @@ -31,39 +83,62 @@ public static void SendEvent(object eventName, string info = "", bool addUserCou } catch (Exception e) { + Logger.Error(e, "Error sending GA event"); } } public static void SendEvent(object eventName, Dictionary parameters, bool addUserCount = false) { - Task.Run(() => + try { - if (!APISecret.Contains("ANALYTICSAPISECRET")) + Task.Run(() => { - var client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://www.google-analytics.com/mp/collect?api_secret=" + APISecret + "&measurement_id=" + MeasurementID); - StringContent? content = null; - int userCount = addUserCount ? 1 : 0; + if (!APISecret.Contains("ANALYTICSAPISECRET")) + { + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "https://www.google-analytics.com/mp/collect?api_secret=" + APISecret + "&measurement_id=" + MeasurementID); - var paramsObject = new Dictionary { { "engagement_time_msec", userCount } }; + var paramsObject = new Dictionary(); - if (parameters != null) - { - foreach (var kvp in parameters) + if (parameters != null) { - paramsObject[kvp.Key] = kvp.Value; + foreach (var kvp in parameters) + { + paramsObject[kvp.Key] = kvp.Value; + } } - } - string paramsJson = JsonConvert.SerializeObject(paramsObject); + var payload = new + { + client_id = clientId, + non_personalized_ads = true, + user_properties = userProperties.Count > 0 ? userProperties : null, + events = new[] + { + new + { + name = eventName.ToString(), + @params = paramsObject + } + } + }; - content = new StringContent($"{{\"client_id\":\"{clientId}\",\"non_personalized_ads\":true,\"events\":[{{\"name\":\"{eventName}\",\"params\":{paramsJson}}}]}}", null, "application/json"); - request.Content = content; - client.Send(request); - } - }); + string jsonPayload = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + var content = new StringContent(jsonPayload, null, "application/json"); + request.Content = content; + client.Send(request); + } + }); + } + catch (Exception e) + { + Logger.Error(e, "Error sending GA event with parameters"); + } } @@ -73,17 +148,46 @@ public static void SendExceptionEvent(object eventName, string message="", bool Logger.Error(eventNameString, message); if (!APISecret.Contains("ANALYTICSAPISECRET")) { - Task.Run(async () => + try { - var client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://www.google-analytics.com/mp/collect?api_secret=" + APISecret + "&measurement_id=" + MeasurementID); - StringContent content = null; - int userCount = addUserCount ? 1 : 0; - content = new StringContent($"{{\"client_id\":\"{clientId}\",\"non_personalized_ads\":true,\"events\":[{{\"name\":\"Exception\",\"params\":{{\"engagement_time_msec\":{userCount},\"{eventName}\":\"{message}\"}}}}]}}", null, "application/json"); - string contentAsString = await content.ReadAsStringAsync(); - request.Content = content; - await client.SendAsync(request); - }); + Task.Run(async () => + { + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "https://www.google-analytics.com/mp/collect?api_secret=" + APISecret + "&measurement_id=" + MeasurementID); + + var payload = new + { + client_id = clientId, + non_personalized_ads = true, + user_properties = userProperties.Count > 0 ? userProperties : null, + events = new[] + { + new + { + name = "Exception", + @params = new Dictionary + { + { "exception_type", eventNameString }, + { "exception_message", message } + } + } + } + }; + + string jsonPayload = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + + var content = new StringContent(jsonPayload, null, "application/json"); + request.Content = content; + await client.SendAsync(request); + }); + } + catch (Exception e) + { + Logger.Error(e, "Error sending GA exception event"); + } } } diff --git a/Appium Wizard/LoadingScreen.cs b/Appium Wizard/LoadingScreen.cs index d76d5a7..6da6488 100644 --- a/Appium Wizard/LoadingScreen.cs +++ b/Appium Wizard/LoadingScreen.cs @@ -42,6 +42,10 @@ public LoadingScreen() Logger.Error(e, "Exception in getting port number"); } string clientId = Database.QueryDataFromGUIDTable(); + + // Set app version as user property for analytics segmentation + GoogleAnalytics.SetUserProperty("app_version", VersionInfo.VersionNumber); + if (clientId.Equals("Empty")) { Guid guid = Guid.NewGuid(); diff --git a/Appium Wizard/MainScreen.cs b/Appium Wizard/MainScreen.cs index 8c53492..1de252a 100644 --- a/Appium Wizard/MainScreen.cs +++ b/Appium Wizard/MainScreen.cs @@ -262,7 +262,7 @@ private void onFormLoad(object sender, EventArgs e) server5WebView.Size = size; openLogsButton.Location = new Point(tabControl1.Right - openLogsButton.Width, tabControl1.Top); } - GoogleAnalytics.SendEvent("App_Version", VersionInfo.VersionNumber); + // App version is now tracked as a user property set in LoadingScreen } private void PerformInitialLayout() From e8cdd88de6140850160e0d0aa660cf3dc628d40f Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 16:18:38 +0530 Subject: [PATCH 11/12] critical update popup --- Appium Wizard/MainScreen.cs | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Appium Wizard/MainScreen.cs b/Appium Wizard/MainScreen.cs index 1de252a..9515726 100644 --- a/Appium Wizard/MainScreen.cs +++ b/Appium Wizard/MainScreen.cs @@ -191,6 +191,17 @@ private void onFormLoad(object sender, EventArgs e) { string updateMessage = "Appium Wizard new version " + latestVersion + " is available for update. Go to \"Help -> Check for updates\" to open the download page."; ShowMessage(updateMessage); + + // Check for mandatory update + if (releaseInfo.ContainsKey("body")) + { + string releaseNotes = releaseInfo["body"]; + if (releaseNotes.Contains("", StringComparison.OrdinalIgnoreCase) || + releaseNotes.Contains("(Mandatory Update)", StringComparison.OrdinalIgnoreCase)) + { + ShowMandatoryUpdateNotification(latestVersion, releaseNotes); + } + } } } var result = Database.QueryDataFromNotificationsTable(); @@ -355,6 +366,47 @@ private void ShowMessage(string message) Controls.Add(tableLayoutPanel1); } + private void ShowMandatoryUpdateNotification(string version, string releaseNotes) + { + try + { + string message = $"⚠️ CRITICAL UPDATE AVAILABLE ⚠️\n\n" + + $"Appium Wizard version {version} is a mandatory update with critical fixes.\n\n" + + $"Release Notes:\n{releaseNotes}\n\n" + + $"It is strongly recommended to update to this version.\n\n" + + $"Would you like to open the download page now?"; + + var result = MessageBox.Show(message, "Critical Update Required", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = "https://github.com/mega6453/AppiumWizard/releases/latest", + UseShellExecute = true + }); + GoogleAnalytics.SendEvent("Mandatory_Update_Download_Opened", version); + } + catch (Exception ex) + { + GoogleAnalytics.SendExceptionEvent("ShowMandatoryUpdateNotification_OpenBrowser", ex.Message); + MessageBox.Show("Could not open browser. Please visit: https://github.com/mega6453/AppiumWizard/releases/latest", + "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + else + { + GoogleAnalytics.SendEvent("Mandatory_Update_Dismissed", version); + } + } + catch (Exception ex) + { + GoogleAnalytics.SendExceptionEvent("ShowMandatoryUpdateNotification", ex.Message); + } + } + private void MainScreen_Shown(object sender, EventArgs e) { try From fd0231e583190b9b2e6890e97b0012ec28098b7c Mon Sep 17 00:00:00 2001 From: "C, Meganathan" Date: Sun, 12 Apr 2026 16:47:25 +0530 Subject: [PATCH 12/12] exception handling on app exit --- Appium Wizard/Common.cs | 39 ++++++++++++- Appium Wizard/MainScreen.cs | 112 ++++++++++++++++++++++++++++++------ 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/Appium Wizard/Common.cs b/Appium Wizard/Common.cs index 47dbd8d..f517bb9 100644 --- a/Appium Wizard/Common.cs +++ b/Appium Wizard/Common.cs @@ -1800,10 +1800,44 @@ private static async Task GetContextAsync(HttpListener list { try { - return await Task.Run(() => listener.GetContext(), token); + // Check if cancellation was requested before attempting to get context + if (token.IsCancellationRequested) + { + return null; + } + + // Check if listener is still listening + if (!listener.IsListening) + { + return null; + } + + return await Task.Run(() => + { + try + { + return listener.GetContext(); + } + catch (HttpListenerException) + { + // Listener was stopped + return null; + } + catch (ObjectDisposedException) + { + // Listener was disposed + return null; + } + }, token); + } + catch (OperationCanceledException) + { + // Task was cancelled + return null; } catch (Exception) { + // Any other exception during shutdown return null; } } @@ -2066,6 +2100,9 @@ public static void StopLogsServer(int serverNumber) // Cancel the token first serverTokens[serverNumber].Cancel(); + // Give a brief moment for the GetContext to detect cancellation + Thread.Sleep(100); + // Stop and close the listener if (serverListeners[serverNumber].IsListening) { diff --git a/Appium Wizard/MainScreen.cs b/Appium Wizard/MainScreen.cs index 9515726..6e5abef 100644 --- a/Appium Wizard/MainScreen.cs +++ b/Appium Wizard/MainScreen.cs @@ -34,6 +34,7 @@ public partial class MainScreen : Form public static Dictionary serverNumberWebView = new Dictionary(); public static Dictionary udidScreenDensity = new Dictionary(); // for android private Timer memoryCleanupTimer; + private Dictionary webViewReloadTimers = new Dictionary(); private const int MEMORY_CLEANUP_INTERVAL = 300000; // 5 minutes private Screen currentScreen; private float currentDpi = 96f; @@ -100,14 +101,26 @@ private async Task ConfigureWebView(int serverNumber, WebView2 webView, string d // 👇 Add blank-page detection with timer var reloadTimer = new Timer { Interval = 3000 }; // check every 3s + webViewReloadTimers[serverNumber] = reloadTimer; // Store timer for cleanup + reloadTimer.Tick += async (s, e) => { try { + // Check if webView is still valid before accessing + if (webView == null || webView.IsDisposed || webView.CoreWebView2 == null) + { + reloadTimer.Stop(); + return; + } + string content = await webView.CoreWebView2.ExecuteScriptAsync("document.body.innerText"); if (string.IsNullOrWhiteSpace(content) || content == "\"\"") { - webView.Reload(); + if (webView != null && !webView.IsDisposed) + { + webView.Reload(); + } } else { @@ -116,7 +129,7 @@ private async Task ConfigureWebView(int serverNumber, WebView2 webView, string d } catch { - // ignore errors (can happen if not ready) + // ignore errors (can happen if not ready or during shutdown) } }; @@ -407,6 +420,21 @@ private void ShowMandatoryUpdateNotification(string version, string releaseNotes } } + private void SafeCloseProgress(CommonProgress commonProgress) + { + try + { + if (commonProgress != null && !commonProgress.IsDisposed) + { + commonProgress.Close(); + } + } + catch (Exception) + { + // Ignore exceptions when closing progress dialog during app shutdown + } + } + private void MainScreen_Shown(object sender, EventArgs e) { try @@ -1801,7 +1829,7 @@ await Task.Run(() => if (isUpdateAvailable) { - commonProgress.Close(); + SafeCloseProgress(commonProgress); var result = MessageBox.Show("Appium Wizard new version " + tagName + " is available.\n\nRelease Notes:\n" + releaseNotes + " \n\nWould you like to open the download page now?", "Check for Updates...", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { @@ -1827,25 +1855,25 @@ await Task.Run(() => } else { - commonProgress.Close(); + SafeCloseProgress(commonProgress); MessageBox.Show("No new updates available at this moment. Please check again later.", "Check for Updates...", MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (Exception ex) { - commonProgress.Close(); + SafeCloseProgress(commonProgress); MessageBox.Show("Failed to check update - Go to https://github.com/mega6453/AppiumWizard and check manually.", "Check for Updates..."); GoogleAnalytics.SendExceptionEvent("checkForUpdatesToolStripMenuItem_Click", ex.Message); } } else { - commonProgress.Close(); + SafeCloseProgress(commonProgress); MessageBox.Show("Internet connection not available. Please connect to internet and try again.", "Check for Updates...", MessageBoxButtons.OK, MessageBoxIcon.Error); GoogleAnalytics.SendEvent("checkForUpdatesToolStripMenuItem_Click", "No_Internet"); } - commonProgress.Close(); + SafeCloseProgress(commonProgress); } private async void updaterToolStripMenuItem_Click(object sender, EventArgs e) @@ -2056,6 +2084,21 @@ private async void onFormClosing(object sender, FormClosingEventArgs e) // Stop all log servers with cleanup Common.StopAllServers(); + commonProgress.UpdateStepLabel("Closing Appium Wizard", "Stopping WebView timers...", 35); + Application.DoEvents(); + + // Stop all WebView reload timers first + foreach (var kvp in webViewReloadTimers) + { + try + { + kvp.Value?.Stop(); + kvp.Value?.Dispose(); + } + catch { } + } + webViewReloadTimers.Clear(); + commonProgress.UpdateStepLabel("Closing Appium Wizard", "Disposing WebView2 controls...", 40); Application.DoEvents(); @@ -2577,7 +2620,21 @@ private void tabControl1_SelectedIndexChanged(object sender, EventArgs e) } openLogsButton.Location = new Point(tabControl1.Right - openLogsButton.Width, tabControl1.Top); openLogsButton.Text = text; - serverNumberWebView[serverNumber].Reload(); + + // Safely reload WebView if it still exists and is not disposed + if (serverNumberWebView.ContainsKey(serverNumber) && + serverNumberWebView[serverNumber] != null && + !serverNumberWebView[serverNumber].IsDisposed) + { + try + { + serverNumberWebView[serverNumber].Reload(); + } + catch (ObjectDisposedException) + { + // WebView was disposed during shutdown, ignore + } + } } @@ -2613,7 +2670,21 @@ public void UpdateOpenLogsButtonText(int serverNumber, bool start) openLogsButton.Visible = false; } } - serverNumberWebView[serverNumber].Reload(); + + // Safely reload WebView if it still exists and is not disposed + if (serverNumberWebView.ContainsKey(serverNumber) && + serverNumberWebView[serverNumber] != null && + !serverNumberWebView[serverNumber].IsDisposed) + { + try + { + serverNumberWebView[serverNumber].Reload(); + } + catch (ObjectDisposedException) + { + // WebView was disposed during shutdown, ignore + } + } } public void UpdateTabText(int serverNumber, int portNumber, bool start) @@ -2826,14 +2897,21 @@ private async Task CleanupWebViewMemory() foreach (var kvp in serverNumberWebView) { var webView = kvp.Value; - if (webView?.CoreWebView2 != null) + if (webView?.CoreWebView2 != null && !webView.IsDisposed) { - // Clear any accumulated DOM content - await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Runtime.evaluate", - "{\"expression\":\"if(document.getElementById('content')) { document.getElementById('content').textContent = document.getElementById('content').textContent.split('\\\\n').slice(-500).join('\\\'); }\"}"); + try + { + // Clear any accumulated DOM content + await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Runtime.evaluate", + "{\"expression\":\"if(document.getElementById('content')) { document.getElementById('content').textContent = document.getElementById('content').textContent.split('\\\\n').slice(-500).join('\\\'); }\"}"); - // Trigger garbage collection in WebView2 - await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("HeapProfiler.collectGarbage", "{}"); + // Trigger garbage collection in WebView2 + await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("HeapProfiler.collectGarbage", "{}"); + } + catch (ObjectDisposedException) + { + // WebView was disposed, skip + } } } @@ -2865,9 +2943,11 @@ private void showCMDToolStripMenuItem_Click(object sender, EventArgs e) WorkingDirectory = serverInstalledPath }; Process.Start(psi); + GoogleAnalytics.SendEvent("showCMDToolStripMenuItem_Click"); } - catch (Exception) + catch (Exception ex) { + GoogleAnalytics.SendExceptionEvent("showCMDToolStripMenuItem_Click", ex.Message); } }