diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8c5eae..868346e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Enhancements +- Add a support for WebSocket client based on Faye::WebSocket::Client. ### Bug fixes diff --git a/Rakefile b/Rakefile index 5975e4de..ac16f881 100644 --- a/Rakefile +++ b/Rakefile @@ -46,6 +46,13 @@ namespace :test do t.libs << 'lib' t.test_files = FileList['test/unit/android/**/*_test.rb'] end + + desc('Run all common related unit tests in test directory') + Rake::TestTask.new(:common) do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/unit/common/**/*_test.rb'] + end end end diff --git a/appium_lib_core.gemspec b/appium_lib_core.gemspec index eb34bc5a..b0dd4183 100644 --- a/appium_lib_core.gemspec +++ b/appium_lib_core.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'selenium-webdriver', '~> 3.5' spec.add_runtime_dependency 'json', '>= 1.8' + spec.add_runtime_dependency 'faye-websocket', '~> 0.10.0' spec.add_development_dependency 'bundler', '~> 1.14' spec.add_development_dependency 'rake', '~> 12.0' diff --git a/lib/appium_lib_core/common.rb b/lib/appium_lib_core/common.rb index 46324080..2600777f 100644 --- a/lib/appium_lib_core/common.rb +++ b/lib/appium_lib_core/common.rb @@ -4,3 +4,4 @@ require_relative 'common/command' require_relative 'common/device' require_relative 'common/base' +require_relative 'common/ws/websocket' diff --git a/lib/appium_lib_core/common/ws/websocket.rb b/lib/appium_lib_core/common/ws/websocket.rb new file mode 100644 index 00000000..459ce71d --- /dev/null +++ b/lib/appium_lib_core/common/ws/websocket.rb @@ -0,0 +1,151 @@ +require 'faye/websocket' +require 'eventmachine' + +module Appium + module Core + class WebSocket + attr_reader :client, :endpoint + + # A websocket client based on Faye::WebSocket::Client . + # Uses eventmachine to wait response from the peer. The eventmachine works on a thread. The thread will exit + # with close method. + # + # @param [String] url: URL to establish web socket connection. If the URL has no port, the client use: + # `ws`: 80, `wss`: 443 ports. + # @param [Array] protocols: An array of strings representing acceptable subprotocols for use over the socket. + # The driver will negotiate one of these to use via the Sec-WebSocket-Protocol header + # if supported by the other peer. Default is nil. + # The protocols is equal to https://github.com/faye/faye-websocket-ruby/ 's one for client. + # @param [Hash] options: Initialize options for Faye client. Read https://github.com/faye/faye-websocket-ruby#initialization-options + # for more details. Default is `{}`. + # + # @example + # ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat") + # ws.client #=> # # An instance of Faye::WebSocket::Client + # ws.message 'some message' #=> nil. Send a message to the peer. + # ws.close #=> Kill the thread which run a eventmachine. + # + def initialize(url:, protocols: nil, options: {}) + @endpoint = url + + @ws_thread = Thread.new do + EM.run do + @client ||= ::Faye::WebSocket::Client.new(url, protocols, options) + + @client.on :open do |_open| + handle_open + end + + @client.on :message do |message| + handle_message_data(message.data) + end + + @client.on :error do |_error| + handle_error + end + + @client.on :close do |close| + handle_close(close.code, close.reason) + end + end + end + end + + # Client + + # + # Sends a ping frame with an optional message and fires the callback when a matching pong is received. + # + # @params [String] message A message to send ping. + # @params [Block] &callback + # + # @example + # ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat") + # ws.ping 'message' + # + def ping(message, &callback) + @client.ping message, &callback + end + + # Accepts either a String or an Array of byte-sized integers and sends a text or binary message over the connection + # to the other peer; binary data must be encoded as an Array. + # + # @params [String|Array] message A message to send a text or binary message over the connection + # + # @example + # ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat") + # ws.send 'happy testing' + # + def send(message) + @client.send message + end + + # Closes the connection, sending the given status code and reason text, both of which are optional. + # + # @params [Integer] code: A status code to send to the peer with close signal. Default is nil. + # @params [String] reason: A reason to send to the peer with close signal. Default is 'close from ruby_lib_core'. + # + # @example + # ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat") + # ws.close reason: 'a something special reason' + # + def close(code: nil, reason: 'close from ruby_lib_core') + if @client.nil? + ::Appium::Logger.warn 'Websocket was closed' + else + @client.close code, reason + end + @ws_thread.exit + end + + # Response from server + + # + # Fires when the socket connection is established. Event has no attributes. + # + # Default is just put a debug message. + # + def handle_open + ::Appium::Logger.debug %W(#{self.class} :open) + end + + # Standard out by default + # In general, users should customise only message_data + + # + # Fires when the socket receives a message. The message gas one `data` attribute and this method can handle the data. + # The data is either a String (for text frames) or an Array of byte-sized integers (for binary frames). + # + # Default is just put a debug message and puts the result on standard out. + # In general, users should override this handler to handle messages from the peer. + # + def handle_message_data(data) + ::Appium::Logger.debug %W(#{self.class} :message #{data}) + $stdout << "#{data}\n" + end + + # + # Fires when there is a protocol error due to bad data sent by the other peer. + # This event is purely informational, you do not need to implement error recovery. + # + # Default is just put a error message. + # + def handle_error + ::Appium::Logger.error %W(#{self.class} :error) + end + + # + # Fires when either the client or the server closes the connection. The method gets `code` and `reason` attributes. + # They expose the status code and message sent by the peer that closed the connection. + # + # Default is just put a error message. + # The methods also clear `client` instance and stop the eventmachine which is called in initialising this class. + # + def handle_close(code, reason) + ::Appium::Logger.debug %W(#{self.class} :close #{code} #{reason}) + @client = nil + EM.stop + end + end # module WebSocket + end # module Core +end # module Appium diff --git a/lib/appium_lib_core/driver.rb b/lib/appium_lib_core/driver.rb index 4602b1d1..0fc1f256 100644 --- a/lib/appium_lib_core/driver.rb +++ b/lib/appium_lib_core/driver.rb @@ -39,6 +39,7 @@ class Driver # Provide Appium::Drive like { appium_lib: { port: 8080 } } # @return [Integer] attr_reader :port + DEFAULT_APPIUM_PORT = 4723 # Return a time wait timeout. 30 seconds is by default. # Wait time for ::Appium::Core::Base::Wait, wait and wait_true @@ -428,7 +429,7 @@ def set_appium_lib_specific_values(appium_lib_opts) @export_session = appium_lib_opts.fetch :export_session, false @export_session_path = appium_lib_opts.fetch :export_session_path, '/tmp/appium_lib_session' - @port = appium_lib_opts.fetch :port, 4723 + @port = appium_lib_opts.fetch :port, DEFAULT_APPIUM_PORT # timeout and interval used in ::Appium::Comm.wait/wait_true @wait_timeout = appium_lib_opts.fetch :wait_timeout, 30 diff --git a/test/unit/common/websocket_test.rb b/test/unit/common/websocket_test.rb new file mode 100644 index 00000000..1bd31ad0 --- /dev/null +++ b/test/unit/common/websocket_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' +require 'webmock/minitest' + +class AppiumLibCoreTest + class WebSocketTest < Minitest::Test + def test_connect_websocket + ws = ::Appium::Core::WebSocket.new(url: 'ws://localhost:9292') + assert_equal nil, ws.client + end + end +end