Skip to content

Commit 598460c

Browse files
authored
implement websocket client (#74)
* introduce websocket client * base websicket client * add simple check * add logcat feature as experimental * rename handlers and make message standard output by default * add docstrings * remove mobile: methods * fix rubocop * add a changelog
1 parent 56a60c6 commit 598460c

File tree

7 files changed

+174
-1
lines changed

7 files changed

+174
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
33

44
## [Unreleased]
55
### Enhancements
6+
- Add a support for WebSocket client based on Faye::WebSocket::Client.
67

78
### Bug fixes
89

Rakefile

+7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ namespace :test do
4646
t.libs << 'lib'
4747
t.test_files = FileList['test/unit/android/**/*_test.rb']
4848
end
49+
50+
desc('Run all common related unit tests in test directory')
51+
Rake::TestTask.new(:common) do |t|
52+
t.libs << 'test'
53+
t.libs << 'lib'
54+
t.test_files = FileList['test/unit/common/**/*_test.rb']
55+
end
4956
end
5057
end
5158

appium_lib_core.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
2424

2525
spec.add_runtime_dependency 'selenium-webdriver', '~> 3.5'
2626
spec.add_runtime_dependency 'json', '>= 1.8'
27+
spec.add_runtime_dependency 'faye-websocket', '~> 0.10.0'
2728

2829
spec.add_development_dependency 'bundler', '~> 1.14'
2930
spec.add_development_dependency 'rake', '~> 12.0'

lib/appium_lib_core/common.rb

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
require_relative 'common/command'
55
require_relative 'common/device'
66
require_relative 'common/base'
7+
require_relative 'common/ws/websocket'
+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
require 'faye/websocket'
2+
require 'eventmachine'
3+
4+
module Appium
5+
module Core
6+
class WebSocket
7+
attr_reader :client, :endpoint
8+
9+
# A websocket client based on Faye::WebSocket::Client .
10+
# Uses eventmachine to wait response from the peer. The eventmachine works on a thread. The thread will exit
11+
# with close method.
12+
#
13+
# @param [String] url: URL to establish web socket connection. If the URL has no port, the client use:
14+
# `ws`: 80, `wss`: 443 ports.
15+
# @param [Array] protocols: An array of strings representing acceptable subprotocols for use over the socket.
16+
# The driver will negotiate one of these to use via the Sec-WebSocket-Protocol header
17+
# if supported by the other peer. Default is nil.
18+
# The protocols is equal to https://github.com/faye/faye-websocket-ruby/ 's one for client.
19+
# @param [Hash] options: Initialize options for Faye client. Read https://github.com/faye/faye-websocket-ruby#initialization-options
20+
# for more details. Default is `{}`.
21+
#
22+
# @example
23+
# ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat")
24+
# ws.client #=> #<Faye::WebSocket::Client:.....> # An instance of Faye::WebSocket::Client
25+
# ws.message 'some message' #=> nil. Send a message to the peer.
26+
# ws.close #=> Kill the thread which run a eventmachine.
27+
#
28+
def initialize(url:, protocols: nil, options: {})
29+
@endpoint = url
30+
31+
@ws_thread = Thread.new do
32+
EM.run do
33+
@client ||= ::Faye::WebSocket::Client.new(url, protocols, options)
34+
35+
@client.on :open do |_open|
36+
handle_open
37+
end
38+
39+
@client.on :message do |message|
40+
handle_message_data(message.data)
41+
end
42+
43+
@client.on :error do |_error|
44+
handle_error
45+
end
46+
47+
@client.on :close do |close|
48+
handle_close(close.code, close.reason)
49+
end
50+
end
51+
end
52+
end
53+
54+
# Client
55+
56+
#
57+
# Sends a ping frame with an optional message and fires the callback when a matching pong is received.
58+
#
59+
# @params [String] message A message to send ping.
60+
# @params [Block] &callback
61+
#
62+
# @example
63+
# ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat")
64+
# ws.ping 'message'
65+
#
66+
def ping(message, &callback)
67+
@client.ping message, &callback
68+
end
69+
70+
# Accepts either a String or an Array of byte-sized integers and sends a text or binary message over the connection
71+
# to the other peer; binary data must be encoded as an Array.
72+
#
73+
# @params [String|Array] message A message to send a text or binary message over the connection
74+
#
75+
# @example
76+
# ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat")
77+
# ws.send 'happy testing'
78+
#
79+
def send(message)
80+
@client.send message
81+
end
82+
83+
# Closes the connection, sending the given status code and reason text, both of which are optional.
84+
#
85+
# @params [Integer] code: A status code to send to the peer with close signal. Default is nil.
86+
# @params [String] reason: A reason to send to the peer with close signal. Default is 'close from ruby_lib_core'.
87+
#
88+
# @example
89+
# ws = WebSocket.new(url: "ws://#{host}:#{port}/ws/session/#{@session_id}/appium/device/logcat")
90+
# ws.close reason: 'a something special reason'
91+
#
92+
def close(code: nil, reason: 'close from ruby_lib_core')
93+
if @client.nil?
94+
::Appium::Logger.warn 'Websocket was closed'
95+
else
96+
@client.close code, reason
97+
end
98+
@ws_thread.exit
99+
end
100+
101+
# Response from server
102+
103+
#
104+
# Fires when the socket connection is established. Event has no attributes.
105+
#
106+
# Default is just put a debug message.
107+
#
108+
def handle_open
109+
::Appium::Logger.debug %W(#{self.class} :open)
110+
end
111+
112+
# Standard out by default
113+
# In general, users should customise only message_data
114+
115+
#
116+
# Fires when the socket receives a message. The message gas one `data` attribute and this method can handle the data.
117+
# The data is either a String (for text frames) or an Array of byte-sized integers (for binary frames).
118+
#
119+
# Default is just put a debug message and puts the result on standard out.
120+
# In general, users should override this handler to handle messages from the peer.
121+
#
122+
def handle_message_data(data)
123+
::Appium::Logger.debug %W(#{self.class} :message #{data})
124+
$stdout << "#{data}\n"
125+
end
126+
127+
#
128+
# Fires when there is a protocol error due to bad data sent by the other peer.
129+
# This event is purely informational, you do not need to implement error recovery.
130+
#
131+
# Default is just put a error message.
132+
#
133+
def handle_error
134+
::Appium::Logger.error %W(#{self.class} :error)
135+
end
136+
137+
#
138+
# Fires when either the client or the server closes the connection. The method gets `code` and `reason` attributes.
139+
# They expose the status code and message sent by the peer that closed the connection.
140+
#
141+
# Default is just put a error message.
142+
# The methods also clear `client` instance and stop the eventmachine which is called in initialising this class.
143+
#
144+
def handle_close(code, reason)
145+
::Appium::Logger.debug %W(#{self.class} :close #{code} #{reason})
146+
@client = nil
147+
EM.stop
148+
end
149+
end # module WebSocket
150+
end # module Core
151+
end # module Appium

lib/appium_lib_core/driver.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Driver
3939
# Provide Appium::Drive like { appium_lib: { port: 8080 } }
4040
# @return [Integer]
4141
attr_reader :port
42+
DEFAULT_APPIUM_PORT = 4723
4243

4344
# Return a time wait timeout. 30 seconds is by default.
4445
# Wait time for ::Appium::Core::Base::Wait, wait and wait_true
@@ -428,7 +429,7 @@ def set_appium_lib_specific_values(appium_lib_opts)
428429
@export_session = appium_lib_opts.fetch :export_session, false
429430
@export_session_path = appium_lib_opts.fetch :export_session_path, '/tmp/appium_lib_session'
430431

431-
@port = appium_lib_opts.fetch :port, 4723
432+
@port = appium_lib_opts.fetch :port, DEFAULT_APPIUM_PORT
432433

433434
# timeout and interval used in ::Appium::Comm.wait/wait_true
434435
@wait_timeout = appium_lib_opts.fetch :wait_timeout, 30

test/unit/common/websocket_test.rb

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require 'test_helper'
2+
require 'webmock/minitest'
3+
4+
class AppiumLibCoreTest
5+
class WebSocketTest < Minitest::Test
6+
def test_connect_websocket
7+
ws = ::Appium::Core::WebSocket.new(url: 'ws://localhost:9292')
8+
assert_equal nil, ws.client
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)