Skip to content
83 changes: 79 additions & 4 deletions Appium Wizard/AppiumServerSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,10 @@ public class AppiumServerSetup
public static bool UpdateStatusInScreenFlag = true;
public static Dictionary<int, int> serverNumberWDAPortNumber = new Dictionary<int, int>();
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

// Cache for element rect values - reduces HTTP calls
private static Dictionary<string, Tuple<Dictionary<string, int>, long>> rectCache = new Dictionary<string, Tuple<Dictionary<string, int>, 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")
{
Expand Down Expand Up @@ -651,18 +656,62 @@ public static string GetAndroidIdFromAppiumServer(int appiumPort, string appiumS
}
}

public static Dictionary<string, int> ElementInfo(string url, string elementId)
// True Async HTTP with Caching - Non-blocking
public static async Task<Dictionary<string, int>> ElementInfoAsync(string url, string elementId)
{
Dictionary<string, int> rectangleDict = new Dictionary<string, int>();

// 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<dynamic>(response.Content);
Expand All @@ -678,15 +727,41 @@ public static Dictionary<string, int> ElementInfo(string url, string elementId)
{ "width", width },
{ "height", height }
};

// Store in cache with current timestamp
lock (rectCacheLock)
{
rectCache[cacheKey] = new Tuple<Dictionary<string, int>, 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<string, int> 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)
Expand Down
39 changes: 38 additions & 1 deletion Appium Wizard/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1800,10 +1800,44 @@ private static async Task<HttpListenerContext> 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;
}
}
Expand Down Expand Up @@ -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)
{
Expand Down
7 changes: 7 additions & 0 deletions Appium Wizard/DeviceInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> dic = new Dictionary<string, string>();
Expand Down
2 changes: 1 addition & 1 deletion Appium Wizard/ExcludeInInstaller/Installer Script.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading
Loading