From 525defbb4b5fbb6d4d88500cc8cd84cd1d833bc1 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:20:58 +0100 Subject: [PATCH 01/30] feat: Implement logcat message broadcasting via WebSocket in AndroidDriver --- .../Appium/Android/AndroidDriver.cs | 89 +++++- .../Interfaces/IListensToLogcatMessages.cs | 93 +++++++ .../Appium/WebSocket/ICanHandleConnects.cs | 41 +++ .../Appium/WebSocket/ICanHandleDisconnects.cs | 41 +++ .../Appium/WebSocket/ICanHandleErrors.cs | 41 +++ .../Appium/WebSocket/ICanHandleMessages.cs | 42 +++ .../Appium/WebSocket/StringWebSocketClient.cs | 254 ++++++++++++++++++ .../Interfaces/IListensToSyslogMessages.cs | 91 +++++++ 8 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs create mode 100644 src/Appium.Net/Appium/WebSocket/ICanHandleConnects.cs create mode 100644 src/Appium.Net/Appium/WebSocket/ICanHandleDisconnects.cs create mode 100644 src/Appium.Net/Appium/WebSocket/ICanHandleErrors.cs create mode 100644 src/Appium.Net/Appium/WebSocket/ICanHandleMessages.cs create mode 100644 src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs create mode 100644 src/Appium.Net/Appium/iOS/Interfaces/IListensToSyslogMessages.cs diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index 911469f9..92724ba2 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -16,6 +16,7 @@ using OpenQA.Selenium.Appium.Enums; using OpenQA.Selenium.Appium.Interfaces; using OpenQA.Selenium.Appium.Service; +using OpenQA.Selenium.Appium.WebSocket; using System; using System.Collections.Generic; using System.Drawing; @@ -28,9 +29,11 @@ public class AndroidDriver : AppiumDriver, IStartsActivity, INetworkActions, IHasClipboard, IHasPerformanceData, ISendsKeyEvents, - IPushesFiles, IHasSettings + IPushesFiles, IHasSettings, IListensToLogcatMessages { private static readonly string Platform = MobilePlatform.Android; + private static readonly StringWebSocketClient LogcatClient = new StringWebSocketClient(); + private const int DefaultAppiumPort = 4723; /// /// Initializes a new instance of the AndroidDriver class @@ -427,5 +430,89 @@ public AppState GetAppState(string appId) => } )?.ToString() ?? throw new InvalidOperationException("ExecuteScript returned null for mobile:queryAppState") ); + + #region Logcat Broadcast + + /// + /// Start logcat messages broadcast via web socket. + /// This method assumes that Appium server is running on localhost and + /// is assigned to the default port (4723). + /// + public void StartLogcatBroadcast() => StartLogcatBroadcast("localhost", DefaultAppiumPort); + + /// + /// Start logcat messages broadcast via web socket. + /// This method assumes that Appium server is assigned to the default port (4723). + /// + /// The name of the host where Appium server is running. + public void StartLogcatBroadcast(string host) => StartLogcatBroadcast(host, DefaultAppiumPort); + + /// + /// Start logcat messages broadcast via web socket. + /// + /// The name of the host where Appium server is running. + /// The port of the host where Appium server is running. + public void StartLogcatBroadcast(string host, int port) + { + ExecuteScript("mobile: startLogsBroadcast", new Dictionary()); + var endpointUri = new Uri($"ws://{host}:{port}/ws/session/{SessionId}/appium/device/logcat"); + LogcatClient.ConnectAsync(endpointUri).Wait(); + } + + /// + /// Adds a new log messages broadcasting handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual log message. + public void AddLogcatMessagesListener(Action handler) => + LogcatClient.MessageHandlers.Add(handler); + + /// + /// Adds a new log broadcasting errors handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual exception instance. + public void AddLogcatErrorsListener(Action handler) => + LogcatClient.ErrorHandlers.Add(handler); + + /// + /// Adds a new log broadcasting connection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully connected to the web socket. + public void AddLogcatConnectionListener(Action handler) => + LogcatClient.ConnectionHandlers.Add(handler); + + /// + /// Adds a new log broadcasting disconnection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully disconnected from the web socket. + public void AddLogcatDisconnectionListener(Action handler) => + LogcatClient.DisconnectionHandlers.Add(handler); + + /// + /// Removes all existing logcat handlers. + /// + public void RemoveAllLogcatListeners() => LogcatClient.RemoveAllHandlers(); + + /// + /// Stops logcat messages broadcast via web socket. + /// + public void StopLogcatBroadcast() + { + ExecuteScript("mobile: stopLogsBroadcast", new Dictionary()); + LogcatClient.DisconnectAsync().Wait(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs new file mode 100644 index 00000000..57c170d7 --- /dev/null +++ b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs @@ -0,0 +1,93 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using OpenQA.Selenium.Appium.WebSocket; +using System; +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.Android.Interfaces +{ + /// + /// Interface for handling Android logcat message broadcasts via WebSocket. + /// + public interface IListensToLogcatMessages + { + /// + /// Start logcat messages broadcast via web socket. + /// This method assumes that Appium server is running on localhost and + /// is assigned to the default port (4723). + /// + void StartLogcatBroadcast(); + + /// + /// Start logcat messages broadcast via web socket. + /// This method assumes that Appium server is assigned to the default port (4723). + /// + /// The name of the host where Appium server is running. + void StartLogcatBroadcast(string host); + + /// + /// Start logcat messages broadcast via web socket. + /// + /// The name of the host where Appium server is running. + /// The port of the host where Appium server is running. + void StartLogcatBroadcast(string host, int port); + + /// + /// Adds a new log messages broadcasting handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual log message. + void AddLogcatMessagesListener(Action handler); + + /// + /// Adds a new log broadcasting errors handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual exception instance. + void AddLogcatErrorsListener(Action handler); + + /// + /// Adds a new log broadcasting connection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully connected to the web socket. + void AddLogcatConnectionListener(Action handler); + + /// + /// Adds a new log broadcasting disconnection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully disconnected from the web socket. + void AddLogcatDisconnectionListener(Action handler); + + /// + /// Removes all existing logcat handlers. + /// + void RemoveAllLogcatListeners(); + + /// + /// Stops logcat messages broadcast via web socket. + /// + void StopLogcatBroadcast(); + } +} diff --git a/src/Appium.Net/Appium/WebSocket/ICanHandleConnects.cs b/src/Appium.Net/Appium/WebSocket/ICanHandleConnects.cs new file mode 100644 index 00000000..18fe66d0 --- /dev/null +++ b/src/Appium.Net/Appium/WebSocket/ICanHandleConnects.cs @@ -0,0 +1,41 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.WebSocket +{ + /// + /// Interface for handling WebSocket connections. + /// + public interface ICanHandleConnects + { + /// + /// Gets the list of web socket connection handlers. + /// + List ConnectionHandlers { get; } + + /// + /// Register a new connection handler. + /// + /// A callback function, which is going to be executed when web socket connection event arrives. + void AddConnectionHandler(Action handler); + + /// + /// Removes existing web socket connection handlers. + /// + void RemoveConnectionHandlers(); + } +} diff --git a/src/Appium.Net/Appium/WebSocket/ICanHandleDisconnects.cs b/src/Appium.Net/Appium/WebSocket/ICanHandleDisconnects.cs new file mode 100644 index 00000000..41af4177 --- /dev/null +++ b/src/Appium.Net/Appium/WebSocket/ICanHandleDisconnects.cs @@ -0,0 +1,41 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.WebSocket +{ + /// + /// Interface for handling WebSocket disconnections. + /// + public interface ICanHandleDisconnects + { + /// + /// Gets the list of web socket disconnection handlers. + /// + List DisconnectionHandlers { get; } + + /// + /// Register a new web socket disconnect handler. + /// + /// A callback function, which is going to be executed when web socket disconnect event arrives. + void AddDisconnectionHandler(Action handler); + + /// + /// Removes existing disconnection handlers. + /// + void RemoveDisconnectionHandlers(); + } +} diff --git a/src/Appium.Net/Appium/WebSocket/ICanHandleErrors.cs b/src/Appium.Net/Appium/WebSocket/ICanHandleErrors.cs new file mode 100644 index 00000000..5b33aaf9 --- /dev/null +++ b/src/Appium.Net/Appium/WebSocket/ICanHandleErrors.cs @@ -0,0 +1,41 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.WebSocket +{ + /// + /// Interface for handling WebSocket errors. + /// + public interface ICanHandleErrors + { + /// + /// Gets the list of web socket error handlers. + /// + List> ErrorHandlers { get; } + + /// + /// Register a new error handler. + /// + /// A callback function, which accepts the received exception instance as a parameter. + void AddErrorHandler(Action handler); + + /// + /// Removes existing error handlers. + /// + void RemoveErrorHandlers(); + } +} diff --git a/src/Appium.Net/Appium/WebSocket/ICanHandleMessages.cs b/src/Appium.Net/Appium/WebSocket/ICanHandleMessages.cs new file mode 100644 index 00000000..4b063e80 --- /dev/null +++ b/src/Appium.Net/Appium/WebSocket/ICanHandleMessages.cs @@ -0,0 +1,42 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.WebSocket +{ + /// + /// Interface for handling WebSocket messages. + /// + /// The type of message to handle. + public interface ICanHandleMessages + { + /// + /// Gets the list of web socket message handlers. + /// + List> MessageHandlers { get; } + + /// + /// Register a new message handler. + /// + /// A callback function, which accepts the received message as a parameter. + void AddMessageHandler(Action handler); + + /// + /// Removes existing message handlers. + /// + void RemoveMessageHandlers(); + } +} diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs new file mode 100644 index 00000000..7d5efefd --- /dev/null +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -0,0 +1,254 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenQA.Selenium.Appium.WebSocket +{ + /// + /// WebSocket client for handling string messages. + /// + public class StringWebSocketClient : ICanHandleMessages, ICanHandleErrors, + ICanHandleConnects, ICanHandleDisconnects + { + private readonly ClientWebSocket _clientWebSocket; + private Uri _endpoint; + private CancellationTokenSource _cancellationTokenSource; + private Task _receiveTask; + + /// + /// Initializes a new instance of the class. + /// + public StringWebSocketClient() + { + _clientWebSocket = new ClientWebSocket(); + MessageHandlers = new List>(); + ErrorHandlers = new List>(); + ConnectionHandlers = new List(); + DisconnectionHandlers = new List(); + } + + /// + /// Gets the list of all registered web socket message handlers. + /// + public List> MessageHandlers { get; } + + /// + /// Gets the list of all registered web socket error handlers. + /// + public List> ErrorHandlers { get; } + + /// + /// Gets the list of all registered web socket connection handlers. + /// + public List ConnectionHandlers { get; } + + /// + /// Gets the list of all registered web socket disconnection handlers. + /// + public List DisconnectionHandlers { get; } + + /// + /// Gets the endpoint URI. + /// + public Uri Endpoint => _endpoint; + + /// + /// Register a new message handler. + /// + /// A callback function, which accepts the received message as a parameter. + public void AddMessageHandler(Action handler) => MessageHandlers.Add(handler); + + /// + /// Removes existing message handlers. + /// + public void RemoveMessageHandlers() => MessageHandlers.Clear(); + + /// + /// Register a new error handler. + /// + /// A callback function, which accepts the received exception instance as a parameter. + public void AddErrorHandler(Action handler) => ErrorHandlers.Add(handler); + + /// + /// Removes existing error handlers. + /// + public void RemoveErrorHandlers() => ErrorHandlers.Clear(); + + /// + /// Register a new connection handler. + /// + /// A callback function, which is going to be executed when web socket connection event arrives. + public void AddConnectionHandler(Action handler) => ConnectionHandlers.Add(handler); + + /// + /// Removes existing web socket connection handlers. + /// + public void RemoveConnectionHandlers() => ConnectionHandlers.Clear(); + + /// + /// Register a new web socket disconnect handler. + /// + /// A callback function, which is going to be executed when web socket disconnect event arrives. + public void AddDisconnectionHandler(Action handler) => DisconnectionHandlers.Add(handler); + + /// + /// Removes existing disconnection handlers. + /// + public void RemoveDisconnectionHandlers() => DisconnectionHandlers.Clear(); + + /// + /// Connects to a WebSocket endpoint. + /// + /// The full address of an endpoint to connect to. Usually starts with 'ws://'. + public async Task ConnectAsync(Uri endpoint) + { + if (_clientWebSocket.State == WebSocketState.Open) + { + if (endpoint.Equals(_endpoint)) + { + return; + } + + await DisconnectAsync(); + } + + try + { + _endpoint = endpoint; + _cancellationTokenSource = new CancellationTokenSource(); + + await _clientWebSocket.ConnectAsync(endpoint, _cancellationTokenSource.Token); + + // Invoke connection handlers + foreach (var handler in ConnectionHandlers) + { + handler?.Invoke(); + } + + // Start receiving messages + _receiveTask = Task.Run(async () => await ReceiveMessagesAsync()); + } + catch (Exception ex) + { + // Invoke error handlers + foreach (var handler in ErrorHandlers) + { + handler?.Invoke(ex); + } + throw new WebDriverException("Failed to connect to WebSocket", ex); + } + } + + /// + /// Disconnects from the WebSocket endpoint. + /// + public async Task DisconnectAsync() + { + if (_clientWebSocket.State == WebSocketState.Open) + { + try + { + _cancellationTokenSource?.Cancel(); + await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + } + catch (Exception) + { + // Ignore errors during close + } + finally + { + // Invoke disconnection handlers + foreach (var handler in DisconnectionHandlers) + { + handler?.Invoke(); + } + } + } + + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + /// + /// Removes all the registered handlers. + /// + public void RemoveAllHandlers() + { + MessageHandlers.Clear(); + ErrorHandlers.Clear(); + ConnectionHandlers.Clear(); + DisconnectionHandlers.Clear(); + } + + private async Task ReceiveMessagesAsync() + { + var buffer = new byte[1024 * 4]; + var messageBuilder = new StringBuilder(); + + try + { + while (_clientWebSocket.State == WebSocketState.Open && !_cancellationTokenSource.Token.IsCancellationRequested) + { + WebSocketReceiveResult result; + + do + { + result = await _clientWebSocket.ReceiveAsync(new ArraySegment(buffer), _cancellationTokenSource.Token); + + if (result.MessageType == WebSocketMessageType.Text) + { + messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await DisconnectAsync(); + return; + } + } + while (!result.EndOfMessage); + + if (messageBuilder.Length > 0) + { + var message = messageBuilder.ToString(); + messageBuilder.Clear(); + + // Invoke message handlers + foreach (var handler in MessageHandlers) + { + handler?.Invoke(message); + } + } + } + } + catch (OperationCanceledException) + { + // Normal cancellation, ignore + } + catch (Exception ex) + { + // Invoke error handlers + foreach (var handler in ErrorHandlers) + { + handler?.Invoke(ex); + } + } + } + } +} diff --git a/src/Appium.Net/Appium/iOS/Interfaces/IListensToSyslogMessages.cs b/src/Appium.Net/Appium/iOS/Interfaces/IListensToSyslogMessages.cs new file mode 100644 index 00000000..54e9d095 --- /dev/null +++ b/src/Appium.Net/Appium/iOS/Interfaces/IListensToSyslogMessages.cs @@ -0,0 +1,91 @@ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//See the NOTICE file distributed with this work for additional +//information regarding copyright ownership. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; + +namespace OpenQA.Selenium.Appium.iOS.Interfaces +{ + /// + /// Interface for handling iOS syslog message broadcasts via WebSocket. + /// + public interface IListensToSyslogMessages + { + /// + /// Start syslog messages broadcast via web socket. + /// This method assumes that Appium server is running on localhost and + /// is assigned to the default port (4723). + /// + void StartSyslogBroadcast(); + + /// + /// Start syslog messages broadcast via web socket. + /// This method assumes that Appium server is assigned to the default port (4723). + /// + /// The name of the host where Appium server is running. + void StartSyslogBroadcast(string host); + + /// + /// Start syslog messages broadcast via web socket. + /// + /// The name of the host where Appium server is running. + /// The port of the host where Appium server is running. + void StartSyslogBroadcast(string host, int port); + + /// + /// Adds a new log messages broadcasting handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual log message. + void AddSyslogMessagesListener(Action handler); + + /// + /// Adds a new log broadcasting errors handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which accepts a single argument, which is the actual exception instance. + void AddSyslogErrorsListener(Action handler); + + /// + /// Adds a new log broadcasting connection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully connected to the web socket. + void AddSyslogConnectionListener(Action handler); + + /// + /// Adds a new log broadcasting disconnection handler. + /// Several handlers might be assigned to a single server. + /// Multiple calls to this method will cause such handler + /// to be called multiple times. + /// + /// A function, which is executed as soon as the client is successfully disconnected from the web socket. + void AddSyslogDisconnectionListener(Action handler); + + /// + /// Removes all existing syslog handlers. + /// + void RemoveAllSyslogListeners(); + + /// + /// Stops syslog messages broadcast via web socket. + /// + void StopSyslogBroadcast(); + } +} From f17435598decf62808754fc69693fd52faac19a1 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:21:17 +0100 Subject: [PATCH 02/30] test: Add integration tests for logcat message broadcasting in AndroidDriver --- .../Android/Session/LogcatBroadcastTests.cs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 test/integration/Android/Session/LogcatBroadcastTests.cs diff --git a/test/integration/Android/Session/LogcatBroadcastTests.cs b/test/integration/Android/Session/LogcatBroadcastTests.cs new file mode 100644 index 00000000..4303f2e0 --- /dev/null +++ b/test/integration/Android/Session/LogcatBroadcastTests.cs @@ -0,0 +1,216 @@ +using System; +using System.Threading; +using Appium.Net.Integration.Tests.helpers; +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; + +namespace Appium.Net.Integration.Tests.Android.Session.Logs +{ + [TestFixture] + internal class LogcatBroadcastTests + { + private AndroidDriver _driver; + private AppiumOptions _androidOptions; + + [OneTimeSetUp] + public void SetUp() + { + _androidOptions = Caps.GetAndroidUIAutomatorCaps(Apps.Get("androidApiDemos")); + _driver = new AndroidDriver( + Env.ServerIsLocal() ? AppiumServers.LocalServiceUri : AppiumServers.RemoteServerUri, + _androidOptions); + } + + [OneTimeTearDown] + public void TearDown() + { + _driver?.Dispose(); + } + + [Test] + public void VerifyLogcatListenerCanBeAssigned() + { + var messageSemaphore = new SemaphoreSlim(0, 1); + var connectionSemaphore = new SemaphoreSlim(0, 1); + var messageReceived = false; + var connectionEstablished = false; + var timeout = TimeSpan.FromSeconds(15); + + // Add listeners + _driver.AddLogcatMessagesListener(msg => + { + Console.WriteLine($"[LOGCAT] {msg}"); + messageReceived = true; + messageSemaphore.Release(); + }); + + _driver.AddLogcatConnectionListener(() => + { + Console.WriteLine("Connected to the logcat web socket"); + connectionEstablished = true; + connectionSemaphore.Release(); + }); + + _driver.AddLogcatDisconnectionListener(() => + { + Console.WriteLine("Disconnected from the logcat web socket"); + }); + + _driver.AddLogcatErrorsListener(ex => + { + Console.WriteLine($"Logcat error: {ex.Message}"); + }); + + try + { + // Start logcat broadcast + _driver.StartLogcatBroadcast(); + + // Wait for connection + Assert.That(connectionSemaphore.Wait(timeout), Is.True, + "Failed to establish WebSocket connection within timeout"); + Assert.That(connectionEstablished, Is.True, + "Connection listener was not invoked"); + + // Trigger some activity to generate log messages + _driver.BackgroundApp(TimeSpan.FromSeconds(1)); + + // Wait for at least one message + Assert.That(messageSemaphore.Wait(timeout), Is.True, + $"Didn't receive any log message after {timeout.TotalSeconds} seconds timeout"); + Assert.That(messageReceived, Is.True, + "Message listener was not invoked"); + } + finally + { + // Clean up + _driver.StopLogcatBroadcast(); + _driver.RemoveAllLogcatListeners(); + messageSemaphore.Dispose(); + connectionSemaphore.Dispose(); + } + } + + [Test] + public void CanStartAndStopLogcatBroadcast() + { + // Should not throw when starting and stopping + Assert.DoesNotThrow(() => + { + _driver.StartLogcatBroadcast(); + _driver.StopLogcatBroadcast(); + }, "Starting and stopping logcat broadcast should not throw exceptions"); + } + + [Test] + public void CanStartLogcatBroadcastWithCustomHost() + { + var host = Env.ServerIsLocal() ? "localhost" : "127.0.0.1"; + var port = 4723; + + Assert.DoesNotThrow(() => + { + _driver.StartLogcatBroadcast(host, port); + _driver.StopLogcatBroadcast(); + }, "Starting logcat broadcast with custom host should not throw exceptions"); + } + + [Test] + public void CanAddAndRemoveMultipleListeners() + { + var messageCount = 0; + var messageSemaphore = new SemaphoreSlim(0, 10); + + Action listener1 = msg => + { + Interlocked.Increment(ref messageCount); + messageSemaphore.Release(); + }; + + Action listener2 = msg => + { + Interlocked.Increment(ref messageCount); + messageSemaphore.Release(); + }; + + try + { + // Add multiple listeners + _driver.AddLogcatMessagesListener(listener1); + _driver.AddLogcatMessagesListener(listener2); + + _driver.StartLogcatBroadcast(); + + // Trigger activity to generate logs + _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); + + // Wait a bit for messages (both listeners should be called) + var received = messageSemaphore.Wait(TimeSpan.FromSeconds(5)); + + if (received) + { + // If we received messages, both listeners should have been invoked + Assert.That(messageCount, Is.GreaterThanOrEqualTo(1), + "At least one listener should have been invoked"); + } + + // Remove all listeners + _driver.RemoveAllLogcatListeners(); + + // Reset counter + messageCount = 0; + + // Trigger more activity + _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); + + // Wait a bit - no new messages should be counted after removing listeners + Thread.Sleep(2000); + + Assert.That(messageCount, Is.EqualTo(0), + "No listeners should be invoked after removing all listeners"); + } + finally + { + _driver.StopLogcatBroadcast(); + _driver.RemoveAllLogcatListeners(); + messageSemaphore.Dispose(); + } + } + + [Test] + public void CanHandleErrorsGracefully() + { + var errorReceived = false; + var errorSemaphore = new SemaphoreSlim(0, 1); + + _driver.AddLogcatErrorsListener(ex => + { + Console.WriteLine($"Error handler invoked: {ex.Message}"); + errorReceived = true; + errorSemaphore.Release(); + }); + + try + { + // Start broadcast normally - should not trigger error handler + _driver.StartLogcatBroadcast(); + + // Give it time to connect + Thread.Sleep(2000); + + // Stop normally + _driver.StopLogcatBroadcast(); + + // Normal operation should not trigger error handler + Assert.That(errorReceived, Is.False, + "Error handler should not be invoked during normal operation"); + } + finally + { + _driver.RemoveAllLogcatListeners(); + errorSemaphore.Dispose(); + } + } + } +} From 1ba8dc68ef338ace3f21c0875c975d1a375d6798 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:21:40 +0100 Subject: [PATCH 03/30] test: Enhance logcat broadcast error handling in LogcatBroadcastTests --- .../Android/Session/LogcatBroadcastTests.cs | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/test/integration/Android/Session/LogcatBroadcastTests.cs b/test/integration/Android/Session/LogcatBroadcastTests.cs index 4303f2e0..b43857f5 100644 --- a/test/integration/Android/Session/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/LogcatBroadcastTests.cs @@ -107,7 +107,7 @@ public void CanStartAndStopLogcatBroadcast() public void CanStartLogcatBroadcastWithCustomHost() { var host = Env.ServerIsLocal() ? "localhost" : "127.0.0.1"; - var port = 4723; + var port = 4723; Assert.DoesNotThrow(() => { @@ -140,7 +140,7 @@ public void CanAddAndRemoveMultipleListeners() _driver.AddLogcatMessagesListener(listener1); _driver.AddLogcatMessagesListener(listener2); - _driver.StartLogcatBroadcast(); + // Trigger activity to generate logs _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); @@ -183,31 +183,60 @@ public void CanHandleErrorsGracefully() { var errorReceived = false; var errorSemaphore = new SemaphoreSlim(0, 1); + Exception capturedError = null; _driver.AddLogcatErrorsListener(ex => { Console.WriteLine($"Error handler invoked: {ex.Message}"); + capturedError = ex; errorReceived = true; errorSemaphore.Release(); }); try { - // Start broadcast normally - should not trigger error handler - _driver.StartLogcatBroadcast(); - - // Give it time to connect - Thread.Sleep(2000); - - // Stop normally - _driver.StopLogcatBroadcast(); - - // Normal operation should not trigger error handler - Assert.That(errorReceived, Is.False, - "Error handler should not be invoked during normal operation"); + // Start broadcast - may fail if endpoint is not available + try + { + _driver.StartLogcatBroadcast(); + + // Give it time to connect + Thread.Sleep(2000); + + // If we got here without errors during connection, that's good + if (!errorReceived) + { + Assert.Pass("Logcat broadcast started successfully without errors"); + } + else + { + // If error occurred during startup, verify error handler was invoked + Assert.That(errorReceived, Is.True, + "Error handler should be invoked when connection fails"); + Assert.That(capturedError, Is.Not.Null, + "Error should be captured by the error handler"); + } + } + catch (AggregateException) + { + // Connection failure is expected in some environments + // Verify that error handler was invoked + Assert.That(errorReceived, Is.True, + "Error handler should be invoked when WebSocket connection fails"); + Assert.That(capturedError, Is.Not.Null, + "Error should be captured by the error handler"); + } } finally { + try + { + _driver.StopLogcatBroadcast(); + } + catch + { + // Ignore errors when stopping if it never started + } _driver.RemoveAllLogcatListeners(); errorSemaphore.Dispose(); } From 29f6149b8b62502211a8cb28e7b1d6903efc67dd Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:21:53 +0100 Subject: [PATCH 04/30] test: Add LogcatBroadcastTests for logcat message broadcasting functionality --- .../Android/Session/{ => Logs}/LogcatBroadcastTests.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/integration/Android/Session/{ => Logs}/LogcatBroadcastTests.cs (100%) diff --git a/test/integration/Android/Session/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs similarity index 100% rename from test/integration/Android/Session/LogcatBroadcastTests.cs rename to test/integration/Android/Session/Logs/LogcatBroadcastTests.cs From 18dbd450997ff20e8f1869eaa50ab87fe37c3ec1 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:37:20 +0100 Subject: [PATCH 05/30] chore: Remove unused using directives in IListensToLogcatMessages interface --- .../Appium/Android/Interfaces/IListensToLogcatMessages.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs index 57c170d7..fd6e7b7b 100644 --- a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs +++ b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs @@ -12,9 +12,7 @@ //See the License for the specific language governing permissions and //limitations under the License. -using OpenQA.Selenium.Appium.WebSocket; using System; -using System.Collections.Generic; namespace OpenQA.Selenium.Appium.Android.Interfaces { From 9a47096e3809ad68e1bcce83f4efd7e991f505ee Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:43:16 +0100 Subject: [PATCH 06/30] refactor: Use 'using' statement for errorSemaphore in CanHandleErrorsGracefully test --- .../Android/Session/Logs/LogcatBroadcastTests.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index b43857f5..26b1067d 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -182,9 +182,8 @@ public void CanAddAndRemoveMultipleListeners() public void CanHandleErrorsGracefully() { var errorReceived = false; - var errorSemaphore = new SemaphoreSlim(0, 1); + using var errorSemaphore = new SemaphoreSlim(0, 1); Exception capturedError = null; - _driver.AddLogcatErrorsListener(ex => { Console.WriteLine($"Error handler invoked: {ex.Message}"); @@ -192,17 +191,15 @@ public void CanHandleErrorsGracefully() errorReceived = true; errorSemaphore.Release(); }); - try { // Start broadcast - may fail if endpoint is not available try { _driver.StartLogcatBroadcast(); - + // Give it time to connect Thread.Sleep(2000); - // If we got here without errors during connection, that's good if (!errorReceived) { @@ -238,8 +235,7 @@ public void CanHandleErrorsGracefully() // Ignore errors when stopping if it never started } _driver.RemoveAllLogcatListeners(); - errorSemaphore.Dispose(); } } } -} +} From aa1dc9e9c3f0890c514ae992927c7d56454cade9 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:45:14 +0100 Subject: [PATCH 07/30] test: Improve error handling in StopLogcatBroadcast method --- test/integration/Android/Session/Logs/LogcatBroadcastTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 26b1067d..20a300e4 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -230,9 +230,9 @@ public void CanHandleErrorsGracefully() { _driver.StopLogcatBroadcast(); } - catch + catch (Exception ex) { - // Ignore errors when stopping if it never started + Console.WriteLine($"Exception during StopLogcatBroadcast: {ex}"); } _driver.RemoveAllLogcatListeners(); } From 9b4e34e85e4dc839a3d048974154884f5401003d Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:49:34 +0100 Subject: [PATCH 08/30] feat: Implement IDisposable interface in StringWebSocketClient for resource management --- .../Appium/WebSocket/StringWebSocketClient.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index 7d5efefd..01ae3609 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -25,7 +25,7 @@ namespace OpenQA.Selenium.Appium.WebSocket /// WebSocket client for handling string messages. /// public class StringWebSocketClient : ICanHandleMessages, ICanHandleErrors, - ICanHandleConnects, ICanHandleDisconnects + ICanHandleConnects, ICanHandleDisconnects, IDisposable { private readonly ClientWebSocket _clientWebSocket; private Uri _endpoint; @@ -250,5 +250,15 @@ private async Task ReceiveMessagesAsync() } } } + /// + /// Disposes the web socket client and releases resources. + /// + public void Dispose() + { + DisconnectAsync().Wait(); + _clientWebSocket?.Dispose(); + _cancellationTokenSource?.Dispose(); + GC.SuppressFinalize(this); + } } } From 8fb8e9870c0b765c3a81471abdedb7b2b7e58b05 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:52:22 +0100 Subject: [PATCH 09/30] feat: Enhance IDisposable implementation in StringWebSocketClient for better resource management --- .../Appium/WebSocket/StringWebSocketClient.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index 01ae3609..b022945d 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -250,15 +250,28 @@ private async Task ReceiveMessagesAsync() } } } + /// /// Disposes the web socket client and releases resources. /// public void Dispose() { - DisconnectAsync().Wait(); - _clientWebSocket?.Dispose(); - _cancellationTokenSource?.Dispose(); + Dispose(true); GC.SuppressFinalize(this); } + + /// + /// Releases the unmanaged resources used by the StringWebSocketClient and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisconnectAsync().Wait(); + _clientWebSocket?.Dispose(); + _cancellationTokenSource?.Dispose(); + } + } } } From 297e8d7eca937b5336842462c00e00124dc43961 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:55:56 +0100 Subject: [PATCH 10/30] refactor: Replace static LogcatClient with instance variable for improved encapsulation --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index 92724ba2..ff85e6da 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -32,7 +32,7 @@ public class AndroidDriver : AppiumDriver, IPushesFiles, IHasSettings, IListensToLogcatMessages { private static readonly string Platform = MobilePlatform.Android; - private static readonly StringWebSocketClient LogcatClient = new StringWebSocketClient(); + private readonly StringWebSocketClient _logcatClient = new(); private const int DefaultAppiumPort = 4723; /// @@ -456,7 +456,7 @@ public void StartLogcatBroadcast(string host, int port) { ExecuteScript("mobile: startLogsBroadcast", new Dictionary()); var endpointUri = new Uri($"ws://{host}:{port}/ws/session/{SessionId}/appium/device/logcat"); - LogcatClient.ConnectAsync(endpointUri).Wait(); + _logcatClient.ConnectAsync(endpointUri).Wait(); } /// @@ -467,7 +467,7 @@ public void StartLogcatBroadcast(string host, int port) /// /// A function, which accepts a single argument, which is the actual log message. public void AddLogcatMessagesListener(Action handler) => - LogcatClient.MessageHandlers.Add(handler); + _logcatClient.MessageHandlers.Add(handler); /// /// Adds a new log broadcasting errors handler. @@ -477,7 +477,7 @@ public void AddLogcatMessagesListener(Action handler) => /// /// A function, which accepts a single argument, which is the actual exception instance. public void AddLogcatErrorsListener(Action handler) => - LogcatClient.ErrorHandlers.Add(handler); + _logcatClient.ErrorHandlers.Add(handler); /// /// Adds a new log broadcasting connection handler. @@ -487,7 +487,7 @@ public void AddLogcatErrorsListener(Action handler) => /// /// A function, which is executed as soon as the client is successfully connected to the web socket. public void AddLogcatConnectionListener(Action handler) => - LogcatClient.ConnectionHandlers.Add(handler); + _logcatClient.ConnectionHandlers.Add(handler); /// /// Adds a new log broadcasting disconnection handler. @@ -497,12 +497,12 @@ public void AddLogcatConnectionListener(Action handler) => /// /// A function, which is executed as soon as the client is successfully disconnected from the web socket. public void AddLogcatDisconnectionListener(Action handler) => - LogcatClient.DisconnectionHandlers.Add(handler); + _logcatClient.DisconnectionHandlers.Add(handler); /// /// Removes all existing logcat handlers. /// - public void RemoveAllLogcatListeners() => LogcatClient.RemoveAllHandlers(); + public void RemoveAllLogcatListeners() => _logcatClient.RemoveAllHandlers(); /// /// Stops logcat messages broadcast via web socket. @@ -510,7 +510,7 @@ public void AddLogcatDisconnectionListener(Action handler) => public void StopLogcatBroadcast() { ExecuteScript("mobile: stopLogsBroadcast", new Dictionary()); - LogcatClient.DisconnectAsync().Wait(); + _logcatClient.DisconnectAsync().Wait(); } #endregion From 2f92495a1616225de3e38a25ffde743d725ee6ed Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 22:59:32 +0100 Subject: [PATCH 11/30] fix: Convert handler collections to arrays before invocation to prevent modification during enumeration --- .../Appium/WebSocket/StringWebSocketClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index b022945d..a2c3a749 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -137,7 +137,7 @@ public async Task ConnectAsync(Uri endpoint) await _clientWebSocket.ConnectAsync(endpoint, _cancellationTokenSource.Token); // Invoke connection handlers - foreach (var handler in ConnectionHandlers) + foreach (var handler in ConnectionHandlers.ToArray()) { handler?.Invoke(); } @@ -148,7 +148,7 @@ public async Task ConnectAsync(Uri endpoint) catch (Exception ex) { // Invoke error handlers - foreach (var handler in ErrorHandlers) + foreach (var handler in ErrorHandlers.ToArray()) { handler?.Invoke(ex); } @@ -175,7 +175,7 @@ public async Task DisconnectAsync() finally { // Invoke disconnection handlers - foreach (var handler in DisconnectionHandlers) + foreach (var handler in DisconnectionHandlers.ToArray()) { handler?.Invoke(); } @@ -230,7 +230,7 @@ private async Task ReceiveMessagesAsync() messageBuilder.Clear(); // Invoke message handlers - foreach (var handler in MessageHandlers) + foreach (var handler in MessageHandlers.ToArray()) { handler?.Invoke(message); } @@ -244,7 +244,7 @@ private async Task ReceiveMessagesAsync() catch (Exception ex) { // Invoke error handlers - foreach (var handler in ErrorHandlers) + foreach (var handler in ErrorHandlers.ToArray()) { handler?.Invoke(ex); } From f5ea24e430459329562a77fe99f8cfa2429bb8df Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sat, 15 Nov 2025 23:02:18 +0100 Subject: [PATCH 12/30] fix: Handle WebSocketException and TaskCanceledException in connection logic for improved error reporting --- .../Appium/WebSocket/StringWebSocketClient.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index a2c3a749..c7e66729 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -145,7 +145,7 @@ public async Task ConnectAsync(Uri endpoint) // Start receiving messages _receiveTask = Task.Run(async () => await ReceiveMessagesAsync()); } - catch (Exception ex) + catch (WebSocketException ex) { // Invoke error handlers foreach (var handler in ErrorHandlers.ToArray()) @@ -154,6 +154,15 @@ public async Task ConnectAsync(Uri endpoint) } throw new WebDriverException("Failed to connect to WebSocket", ex); } + catch (TaskCanceledException ex) + { + // Invoke error handlers + foreach (var handler in ErrorHandlers.ToArray()) + { + handler?.Invoke(ex); + } + throw new WebDriverException("WebSocket connection was cancelled", ex); + } } /// From 96e42b23746c85dabb6d0bef375116b94996544f Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:32:00 +0100 Subject: [PATCH 13/30] feat: Update StartLogcatBroadcast methods to async for improved performance and responsiveness --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 9 +++++---- .../Android/Interfaces/IListensToLogcatMessages.cs | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index ff85e6da..a05f7a85 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -22,6 +22,7 @@ using System.Drawing; using System.Linq; using OpenQA.Selenium.Appium.Android.Enums; +using System.Threading.Tasks; namespace OpenQA.Selenium.Appium.Android { @@ -438,25 +439,25 @@ public AppState GetAppState(string appId) => /// This method assumes that Appium server is running on localhost and /// is assigned to the default port (4723). /// - public void StartLogcatBroadcast() => StartLogcatBroadcast("localhost", DefaultAppiumPort); + public async Task StartLogcatBroadcast() => await StartLogcatBroadcast("localhost", DefaultAppiumPort); /// /// Start logcat messages broadcast via web socket. /// This method assumes that Appium server is assigned to the default port (4723). /// /// The name of the host where Appium server is running. - public void StartLogcatBroadcast(string host) => StartLogcatBroadcast(host, DefaultAppiumPort); + public async Task StartLogcatBroadcast(string host) => await StartLogcatBroadcast(host, DefaultAppiumPort); /// /// Start logcat messages broadcast via web socket. /// /// The name of the host where Appium server is running. /// The port of the host where Appium server is running. - public void StartLogcatBroadcast(string host, int port) + public async Task StartLogcatBroadcast(string host, int port) { ExecuteScript("mobile: startLogsBroadcast", new Dictionary()); var endpointUri = new Uri($"ws://{host}:{port}/ws/session/{SessionId}/appium/device/logcat"); - _logcatClient.ConnectAsync(endpointUri).Wait(); + await _logcatClient.ConnectAsync(endpointUri); } /// diff --git a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs index fd6e7b7b..9496fbc5 100644 --- a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs +++ b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs @@ -13,6 +13,7 @@ //limitations under the License. using System; +using System.Threading.Tasks; namespace OpenQA.Selenium.Appium.Android.Interfaces { @@ -26,21 +27,21 @@ public interface IListensToLogcatMessages /// This method assumes that Appium server is running on localhost and /// is assigned to the default port (4723). /// - void StartLogcatBroadcast(); + Task StartLogcatBroadcast(); /// /// Start logcat messages broadcast via web socket. /// This method assumes that Appium server is assigned to the default port (4723). /// /// The name of the host where Appium server is running. - void StartLogcatBroadcast(string host); + Task StartLogcatBroadcast(string host); /// /// Start logcat messages broadcast via web socket. /// /// The name of the host where Appium server is running. /// The port of the host where Appium server is running. - void StartLogcatBroadcast(string host, int port); + Task StartLogcatBroadcast(string host, int port); /// /// Adds a new log messages broadcasting handler. From 6b6f00b69ba35a531f14eda4d6de019dd3961ef4 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:36:27 +0100 Subject: [PATCH 14/30] feat: Update CanAddAndRemoveMultipleListeners test to use async for improved performance --- .../Android/Session/Logs/LogcatBroadcastTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 20a300e4..bebd6365 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; using Appium.Net.Integration.Tests.helpers; using NUnit.Framework; using OpenQA.Selenium.Appium; @@ -117,7 +118,7 @@ public void CanStartLogcatBroadcastWithCustomHost() } [Test] - public void CanAddAndRemoveMultipleListeners() + public async Task CanAddAndRemoveMultipleListeners() { var messageCount = 0; var messageSemaphore = new SemaphoreSlim(0, 10); @@ -135,7 +136,8 @@ public void CanAddAndRemoveMultipleListeners() }; try - { + { + await _driver.StartLogcatBroadcast(); // Add multiple listeners _driver.AddLogcatMessagesListener(listener1); _driver.AddLogcatMessagesListener(listener2); From aba9987d42a8ea4f9fd061853f3716337e898678 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:37:14 +0100 Subject: [PATCH 15/30] fix: Remove unnecessary blank lines in CanAddAndRemoveMultipleListeners test for cleaner code --- test/integration/Android/Session/Logs/LogcatBroadcastTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index bebd6365..8111f934 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -142,8 +142,6 @@ public async Task CanAddAndRemoveMultipleListeners() _driver.AddLogcatMessagesListener(listener1); _driver.AddLogcatMessagesListener(listener2); - - // Trigger activity to generate logs _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); From 13139078f380d0a97be36789240dccc996dba63f Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:40:32 +0100 Subject: [PATCH 16/30] feat: Update VerifyLogcatListenerCanBeAssigned test to async for improved performance and responsiveness --- .../Session/Logs/LogcatBroadcastTests.cs | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 8111f934..619ddd68 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -30,14 +30,13 @@ public void TearDown() } [Test] - public void VerifyLogcatListenerCanBeAssigned() + public async Task VerifyLogcatListenerCanBeAssigned() { - var messageSemaphore = new SemaphoreSlim(0, 1); - var connectionSemaphore = new SemaphoreSlim(0, 1); + using var messageSemaphore = new SemaphoreSlim(0, 1); + using var connectionSemaphore = new SemaphoreSlim(0, 1); var messageReceived = false; var connectionEstablished = false; var timeout = TimeSpan.FromSeconds(15); - // Add listeners _driver.AddLogcatMessagesListener(msg => { @@ -45,42 +44,35 @@ public void VerifyLogcatListenerCanBeAssigned() messageReceived = true; messageSemaphore.Release(); }); - _driver.AddLogcatConnectionListener(() => { Console.WriteLine("Connected to the logcat web socket"); connectionEstablished = true; connectionSemaphore.Release(); }); - _driver.AddLogcatDisconnectionListener(() => { Console.WriteLine("Disconnected from the logcat web socket"); }); - _driver.AddLogcatErrorsListener(ex => { Console.WriteLine($"Logcat error: {ex.Message}"); }); - try { // Start logcat broadcast - _driver.StartLogcatBroadcast(); - + await _driver.StartLogcatBroadcast(); // Wait for connection - Assert.That(connectionSemaphore.Wait(timeout), Is.True, + Assert.That(connectionSemaphore.Wait(timeout), Is.True, "Failed to establish WebSocket connection within timeout"); - Assert.That(connectionEstablished, Is.True, + Assert.That(connectionEstablished, Is.True, "Connection listener was not invoked"); - // Trigger some activity to generate log messages _driver.BackgroundApp(TimeSpan.FromSeconds(1)); - // Wait for at least one message Assert.That(messageSemaphore.Wait(timeout), Is.True, $"Didn't receive any log message after {timeout.TotalSeconds} seconds timeout"); - Assert.That(messageReceived, Is.True, + Assert.That(messageReceived, Is.True, "Message listener was not invoked"); } finally @@ -88,8 +80,6 @@ public void VerifyLogcatListenerCanBeAssigned() // Clean up _driver.StopLogcatBroadcast(); _driver.RemoveAllLogcatListeners(); - messageSemaphore.Dispose(); - connectionSemaphore.Dispose(); } } From b373cbb059ed484c2fe1e3354f0cc62b35de1fd7 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:44:06 +0100 Subject: [PATCH 17/30] refactor: Simplify CanAddAndRemoveMultipleListeners test by using 'using' for messageSemaphore and removing unnecessary variable declarations --- .../Session/Logs/LogcatBroadcastTests.cs | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 619ddd68..c9a4bf31 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -111,22 +111,19 @@ public void CanStartLogcatBroadcastWithCustomHost() public async Task CanAddAndRemoveMultipleListeners() { var messageCount = 0; - var messageSemaphore = new SemaphoreSlim(0, 10); - - Action listener1 = msg => + using var messageSemaphore = new SemaphoreSlim(0, 10); + void listener1(string msg) { Interlocked.Increment(ref messageCount); messageSemaphore.Release(); - }; - - Action listener2 = msg => + } + void listener2(string msg) { Interlocked.Increment(ref messageCount); messageSemaphore.Release(); - }; - + } try - { + { await _driver.StartLogcatBroadcast(); // Add multiple listeners _driver.AddLogcatMessagesListener(listener1); @@ -134,29 +131,22 @@ public async Task CanAddAndRemoveMultipleListeners() // Trigger activity to generate logs _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); - // Wait a bit for messages (both listeners should be called) var received = messageSemaphore.Wait(TimeSpan.FromSeconds(5)); - if (received) { // If we received messages, both listeners should have been invoked Assert.That(messageCount, Is.GreaterThanOrEqualTo(1), "At least one listener should have been invoked"); } - // Remove all listeners _driver.RemoveAllLogcatListeners(); - // Reset counter messageCount = 0; - // Trigger more activity _driver.BackgroundApp(TimeSpan.FromMilliseconds(500)); - // Wait a bit - no new messages should be counted after removing listeners Thread.Sleep(2000); - Assert.That(messageCount, Is.EqualTo(0), "No listeners should be invoked after removing all listeners"); } @@ -164,7 +154,6 @@ public async Task CanAddAndRemoveMultipleListeners() { _driver.StopLogcatBroadcast(); _driver.RemoveAllLogcatListeners(); - messageSemaphore.Dispose(); } } From edcd9955fcc3366b8d3eb9321fe6031d11dd07da Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:56:35 +0100 Subject: [PATCH 18/30] feat: Update LogcatBroadcast tests to use async for improved performance and responsiveness --- .../Appium/WebSocket/StringWebSocketClient.cs | 10 ++++++--- .../Session/Logs/LogcatBroadcastTests.cs | 22 +++++++------------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index c7e66729..414534d0 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -143,7 +143,7 @@ public async Task ConnectAsync(Uri endpoint) } // Start receiving messages - _receiveTask = Task.Run(async () => await ReceiveMessagesAsync()); + _receiveTask = Task.Run(ReceiveMessagesAsync); } catch (WebSocketException ex) { @@ -177,9 +177,13 @@ public async Task DisconnectAsync() _cancellationTokenSource?.Cancel(); await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); } - catch (Exception) + catch (Exception ex) { - // Ignore errors during close + // Invoke error handlers for errors during close + foreach (var handler in ErrorHandlers) + { + handler?.Invoke(ex); + } } finally { diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index c9a4bf31..415f415c 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -84,27 +84,21 @@ public async Task VerifyLogcatListenerCanBeAssigned() } [Test] - public void CanStartAndStopLogcatBroadcast() + public async Task CanStartAndStopLogcatBroadcast() { // Should not throw when starting and stopping - Assert.DoesNotThrow(() => - { - _driver.StartLogcatBroadcast(); - _driver.StopLogcatBroadcast(); - }, "Starting and stopping logcat broadcast should not throw exceptions"); + await _driver.StartLogcatBroadcast(); + _driver.StopLogcatBroadcast(); } [Test] - public void CanStartLogcatBroadcastWithCustomHost() + public async Task CanStartLogcatBroadcastWithCustomHost() { var host = Env.ServerIsLocal() ? "localhost" : "127.0.0.1"; var port = 4723; - Assert.DoesNotThrow(() => - { - _driver.StartLogcatBroadcast(host, port); - _driver.StopLogcatBroadcast(); - }, "Starting logcat broadcast with custom host should not throw exceptions"); + await _driver.StartLogcatBroadcast(host, port); + _driver.StopLogcatBroadcast(); } [Test] @@ -158,7 +152,7 @@ void listener2(string msg) } [Test] - public void CanHandleErrorsGracefully() + public async Task CanHandleErrorsGracefully() { var errorReceived = false; using var errorSemaphore = new SemaphoreSlim(0, 1); @@ -175,7 +169,7 @@ public void CanHandleErrorsGracefully() // Start broadcast - may fail if endpoint is not available try { - _driver.StartLogcatBroadcast(); + await _driver.StartLogcatBroadcast(); // Give it time to connect Thread.Sleep(2000); From 147506a6a5a4d1f49e9f0909905d68c7d777ed55 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 17:59:34 +0100 Subject: [PATCH 19/30] feat: Ensure receive task completion before closing WebSocket connection --- src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index 414534d0..de37fca9 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -176,6 +176,12 @@ public async Task DisconnectAsync() { _cancellationTokenSource?.Cancel(); await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + + // Wait for receive task to complete + if (_receiveTask != null) + { + await _receiveTask; + } } catch (Exception ex) { From bcfe60357c9186098c1754a43c588b36c10fdee1 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:06:30 +0100 Subject: [PATCH 20/30] feat: Enhance WebSocket connection handling with semaphore for thread safety --- .../Appium/WebSocket/StringWebSocketClient.cs | 102 ++++++++++++------ 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index de37fca9..4b91ff08 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -27,7 +27,8 @@ namespace OpenQA.Selenium.Appium.WebSocket public class StringWebSocketClient : ICanHandleMessages, ICanHandleErrors, ICanHandleConnects, ICanHandleDisconnects, IDisposable { - private readonly ClientWebSocket _clientWebSocket; + private ClientWebSocket _clientWebSocket; + private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1); private Uri _endpoint; private CancellationTokenSource _cancellationTokenSource; private Task _receiveTask; @@ -119,49 +120,67 @@ public StringWebSocketClient() /// The full address of an endpoint to connect to. Usually starts with 'ws://'. public async Task ConnectAsync(Uri endpoint) { - if (_clientWebSocket.State == WebSocketState.Open) + await _connectionLock.WaitAsync(); + try { - if (endpoint.Equals(_endpoint)) + // If websocket is already open and connected to the same endpoint, return + if (_clientWebSocket?.State == WebSocketState.Open) { - return; - } + if (endpoint.Equals(_endpoint)) + { + return; + } - await DisconnectAsync(); - } + await DisconnectInternalAsync(); + } - try - { - _endpoint = endpoint; - _cancellationTokenSource = new CancellationTokenSource(); - - await _clientWebSocket.ConnectAsync(endpoint, _cancellationTokenSource.Token); - - // Invoke connection handlers - foreach (var handler in ConnectionHandlers.ToArray()) + // Recreate ClientWebSocket if it's disposed or in a non-connectable state + if (_clientWebSocket == null || + _clientWebSocket.State == WebSocketState.Closed || + _clientWebSocket.State == WebSocketState.Aborted) { - handler?.Invoke(); + _clientWebSocket?.Dispose(); + _clientWebSocket = new ClientWebSocket(); } - // Start receiving messages - _receiveTask = Task.Run(ReceiveMessagesAsync); - } - catch (WebSocketException ex) - { - // Invoke error handlers - foreach (var handler in ErrorHandlers.ToArray()) + try { - handler?.Invoke(ex); + _endpoint = endpoint; + _cancellationTokenSource = new CancellationTokenSource(); + + await _clientWebSocket.ConnectAsync(endpoint, _cancellationTokenSource.Token); + + // Invoke connection handlers + foreach (var handler in ConnectionHandlers.ToArray()) + { + handler?.Invoke(); + } + + // Start receiving messages + _receiveTask = Task.Run(ReceiveMessagesAsync); } - throw new WebDriverException("Failed to connect to WebSocket", ex); - } - catch (TaskCanceledException ex) - { - // Invoke error handlers - foreach (var handler in ErrorHandlers.ToArray()) + catch (WebSocketException ex) { - handler?.Invoke(ex); + // Invoke error handlers + foreach (var handler in ErrorHandlers.ToArray()) + { + handler?.Invoke(ex); + } + throw new WebDriverException("Failed to connect to WebSocket", ex); + } + catch (TaskCanceledException ex) + { + // Invoke error handlers + foreach (var handler in ErrorHandlers.ToArray()) + { + handler?.Invoke(ex); + } + throw new WebDriverException("WebSocket connection was cancelled", ex); } - throw new WebDriverException("WebSocket connection was cancelled", ex); + } + finally + { + _connectionLock.Release(); } } @@ -169,6 +188,22 @@ public async Task ConnectAsync(Uri endpoint) /// Disconnects from the WebSocket endpoint. /// public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync(); + try + { + await DisconnectInternalAsync(); + } + finally + { + _connectionLock.Release(); + } + } + + /// + /// Internal disconnect method without lock (called when already holding the lock). + /// + private async Task DisconnectInternalAsync() { if (_clientWebSocket.State == WebSocketState.Open) { @@ -290,6 +325,7 @@ protected virtual void Dispose(bool disposing) DisconnectAsync().Wait(); _clientWebSocket?.Dispose(); _cancellationTokenSource?.Dispose(); + _connectionLock?.Dispose(); } } } From 9817502a474bd6f5aa8d97a32a6a46bba0cf5228 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:07:12 +0100 Subject: [PATCH 21/30] fix: Remove unnecessary whitespace in CanStartLogcatBroadcastWithCustomHost test --- test/integration/Android/Session/Logs/LogcatBroadcastTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 415f415c..8adb682b 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -95,7 +95,7 @@ public async Task CanStartAndStopLogcatBroadcast() public async Task CanStartLogcatBroadcastWithCustomHost() { var host = Env.ServerIsLocal() ? "localhost" : "127.0.0.1"; - var port = 4723; + var port = 4723; await _driver.StartLogcatBroadcast(host, port); _driver.StopLogcatBroadcast(); From 09701b33db4d1eb78952f9b8104799f12d6dfb7c Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:11:41 +0100 Subject: [PATCH 22/30] refactor: Replace direct handler additions with dedicated methods in AndroidDriver --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index a05f7a85..915366df 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -468,7 +468,7 @@ public async Task StartLogcatBroadcast(string host, int port) /// /// A function, which accepts a single argument, which is the actual log message. public void AddLogcatMessagesListener(Action handler) => - _logcatClient.MessageHandlers.Add(handler); + _logcatClient.AddMessageHandler(handler); /// /// Adds a new log broadcasting errors handler. @@ -478,7 +478,7 @@ public void AddLogcatMessagesListener(Action handler) => /// /// A function, which accepts a single argument, which is the actual exception instance. public void AddLogcatErrorsListener(Action handler) => - _logcatClient.ErrorHandlers.Add(handler); + _logcatClient.AddErrorHandler(handler); /// /// Adds a new log broadcasting connection handler. @@ -488,7 +488,7 @@ public void AddLogcatErrorsListener(Action handler) => /// /// A function, which is executed as soon as the client is successfully connected to the web socket. public void AddLogcatConnectionListener(Action handler) => - _logcatClient.ConnectionHandlers.Add(handler); + _logcatClient.AddConnectionHandler(handler); /// /// Adds a new log broadcasting disconnection handler. @@ -498,7 +498,7 @@ public void AddLogcatConnectionListener(Action handler) => /// /// A function, which is executed as soon as the client is successfully disconnected from the web socket. public void AddLogcatDisconnectionListener(Action handler) => - _logcatClient.DisconnectionHandlers.Add(handler); + _logcatClient.AddDisconnectionHandler(handler); /// /// Removes all existing logcat handlers. From 4b5fe718dbaf1cfe077e60c52a7612f45be3d86b Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:14:37 +0100 Subject: [PATCH 23/30] fix: Remove initialization of ClientWebSocket in StringWebSocketClient constructor --- src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index 4b91ff08..c9bf5261 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -38,7 +38,6 @@ public class StringWebSocketClient : ICanHandleMessages, ICanHandleError /// public StringWebSocketClient() { - _clientWebSocket = new ClientWebSocket(); MessageHandlers = new List>(); ErrorHandlers = new List>(); ConnectionHandlers = new List(); From 4c98fbb55a1c3e2cf1b8814e1bcfdf42f854f56b Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:21:37 +0100 Subject: [PATCH 24/30] feat: Update StopLogcatBroadcast method to be asynchronous --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 4 ++-- .../Appium/Android/Interfaces/IListensToLogcatMessages.cs | 2 +- .../Android/Session/Logs/LogcatBroadcastTests.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index 915366df..4a9ed877 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -508,10 +508,10 @@ public void AddLogcatDisconnectionListener(Action handler) => /// /// Stops logcat messages broadcast via web socket. /// - public void StopLogcatBroadcast() + public async Task StopLogcatBroadcast() { ExecuteScript("mobile: stopLogsBroadcast", new Dictionary()); - _logcatClient.DisconnectAsync().Wait(); + await _logcatClient.DisconnectAsync(); } #endregion diff --git a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs index 9496fbc5..10f128a0 100644 --- a/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs +++ b/src/Appium.Net/Appium/Android/Interfaces/IListensToLogcatMessages.cs @@ -87,6 +87,6 @@ public interface IListensToLogcatMessages /// /// Stops logcat messages broadcast via web socket. /// - void StopLogcatBroadcast(); + Task StopLogcatBroadcast(); } } diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 8adb682b..9a9b1c36 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -78,7 +78,7 @@ public async Task VerifyLogcatListenerCanBeAssigned() finally { // Clean up - _driver.StopLogcatBroadcast(); + await _driver.StopLogcatBroadcast(); _driver.RemoveAllLogcatListeners(); } } @@ -88,7 +88,7 @@ public async Task CanStartAndStopLogcatBroadcast() { // Should not throw when starting and stopping await _driver.StartLogcatBroadcast(); - _driver.StopLogcatBroadcast(); + await _driver.StopLogcatBroadcast(); } [Test] @@ -98,7 +98,7 @@ public async Task CanStartLogcatBroadcastWithCustomHost() var port = 4723; await _driver.StartLogcatBroadcast(host, port); - _driver.StopLogcatBroadcast(); + await _driver.StopLogcatBroadcast(); } [Test] @@ -201,7 +201,7 @@ public async Task CanHandleErrorsGracefully() { try { - _driver.StopLogcatBroadcast(); + await _driver.StopLogcatBroadcast(); } catch (Exception ex) { From 1a031639aea368cfb4ef0e07353efb9c03932ed7 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 18:26:05 +0100 Subject: [PATCH 25/30] fix: Use ToArray() on ErrorHandlers to avoid modification during iteration --- src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index c9bf5261..6ed2962f 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -220,7 +220,7 @@ private async Task DisconnectInternalAsync() catch (Exception ex) { // Invoke error handlers for errors during close - foreach (var handler in ErrorHandlers) + foreach (var handler in ErrorHandlers.ToArray()) { handler?.Invoke(ex); } From 8b4f0006c8a1c9c6a438fe77181a98415cb39dba Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Sun, 16 Nov 2025 22:39:22 +0100 Subject: [PATCH 26/30] feat: Make StopLogcatBroadcast method asynchronous in tests --- test/integration/Android/Session/Logs/LogcatBroadcastTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 9a9b1c36..632c443b 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -146,7 +146,7 @@ void listener2(string msg) } finally { - _driver.StopLogcatBroadcast(); + await _driver.StopLogcatBroadcast(); _driver.RemoveAllLogcatListeners(); } } From a1458c91ed8272abe18db6c4a9bcec815f4ad68b Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Thu, 20 Nov 2025 20:28:25 +0100 Subject: [PATCH 27/30] chore: Add remarks to StartLogcatBroadcast methods regarding temporary WebSocket implementation --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index 4a9ed877..91605159 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -439,6 +439,11 @@ public AppState GetAppState(string appId) => /// This method assumes that Appium server is running on localhost and /// is assigned to the default port (4723). /// + /// + /// This implementation uses a custom WebSocket endpoint and is temporary. + /// In the future, this functionality will be replaced with WebDriver BiDi log events + /// when BiDi support for Android device logs becomes available. + /// public async Task StartLogcatBroadcast() => await StartLogcatBroadcast("localhost", DefaultAppiumPort); /// @@ -446,6 +451,11 @@ public AppState GetAppState(string appId) => /// This method assumes that Appium server is assigned to the default port (4723). /// /// The name of the host where Appium server is running. + /// + /// This implementation uses a custom WebSocket endpoint and is temporary. + /// In the future, this functionality will be replaced with WebDriver BiDi log events + /// when BiDi support for Android device logs becomes available. + /// public async Task StartLogcatBroadcast(string host) => await StartLogcatBroadcast(host, DefaultAppiumPort); /// @@ -453,6 +463,11 @@ public AppState GetAppState(string appId) => /// /// The name of the host where Appium server is running. /// The port of the host where Appium server is running. + /// + /// This implementation uses a custom WebSocket endpoint and is temporary. + /// In the future, this functionality will be replaced with WebDriver BiDi log events + /// when BiDi support for Android device logs becomes available. + /// public async Task StartLogcatBroadcast(string host, int port) { ExecuteScript("mobile: startLogsBroadcast", new Dictionary()); From d0269c4509c98659dd34879ae7dc76274ed53aff Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Tue, 25 Nov 2025 18:57:33 +0100 Subject: [PATCH 28/30] fix: Update StartLogcatBroadcast method to use loopback address --- src/Appium.Net/Appium/Android/AndroidDriver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appium.Net/Appium/Android/AndroidDriver.cs b/src/Appium.Net/Appium/Android/AndroidDriver.cs index 91605159..1df33546 100644 --- a/src/Appium.Net/Appium/Android/AndroidDriver.cs +++ b/src/Appium.Net/Appium/Android/AndroidDriver.cs @@ -444,7 +444,7 @@ public AppState GetAppState(string appId) => /// In the future, this functionality will be replaced with WebDriver BiDi log events /// when BiDi support for Android device logs becomes available. /// - public async Task StartLogcatBroadcast() => await StartLogcatBroadcast("localhost", DefaultAppiumPort); + public async Task StartLogcatBroadcast() => await StartLogcatBroadcast("127.0.0.1", DefaultAppiumPort); /// /// Start logcat messages broadcast via web socket. From 38a931493b9c287cc4046da35575cc76109dd767 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Tue, 25 Nov 2025 18:57:38 +0100 Subject: [PATCH 29/30] fix: Enhance WebDriverException message with timestamp for WebSocket connection failures --- src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs index 6ed2962f..dd755ff4 100644 --- a/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs +++ b/src/Appium.Net/Appium/WebSocket/StringWebSocketClient.cs @@ -165,7 +165,7 @@ public async Task ConnectAsync(Uri endpoint) { handler?.Invoke(ex); } - throw new WebDriverException("Failed to connect to WebSocket", ex); + throw new WebDriverException($"Failed to connect to WebSocket at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC", ex); } catch (TaskCanceledException ex) { From 2694cbfe99a00f438c219985632706b846c5a0a1 Mon Sep 17 00:00:00 2001 From: Dor-bl Date: Tue, 25 Nov 2025 18:59:00 +0100 Subject: [PATCH 30/30] fix: Update host variable in CanStartLogcatBroadcastWithCustomHost test to use a fixed IP address --- test/integration/Android/Session/Logs/LogcatBroadcastTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs index 632c443b..b9ebc2f2 100644 --- a/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs +++ b/test/integration/Android/Session/Logs/LogcatBroadcastTests.cs @@ -94,7 +94,7 @@ public async Task CanStartAndStopLogcatBroadcast() [Test] public async Task CanStartLogcatBroadcastWithCustomHost() { - var host = Env.ServerIsLocal() ? "localhost" : "127.0.0.1"; + var host = "127.0.0.1"; var port = 4723; await _driver.StartLogcatBroadcast(host, port);