Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions appium_lib_core.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions lib/appium_lib_core/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
require_relative 'common/command'
require_relative 'common/device'
require_relative 'common/base'
require_relative 'common/ws/websocket'
151 changes: 151 additions & 0 deletions lib/appium_lib_core/common/ws/websocket.rb
Original file line number Diff line number Diff line change
@@ -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 #=> #<Faye::WebSocket::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
3 changes: 2 additions & 1 deletion lib/appium_lib_core/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions test/unit/common/websocket_test.rb
Original file line number Diff line number Diff line change
@@ -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