Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/rb/lib/selenium/server.rb
4021 views
1
# frozen_string_literal: true
2
3
# Licensed to the Software Freedom Conservancy (SFC) under one
4
# or more contributor license agreements. See the NOTICE file
5
# distributed with this work for additional information
6
# regarding copyright ownership. The SFC licenses this file
7
# to you under the Apache License, Version 2.0 (the
8
# "License"); you may not use this file except in compliance
9
# with the License. You may obtain a copy of the License at
10
#
11
# http://www.apache.org/licenses/LICENSE-2.0
12
#
13
# Unless required by applicable law or agreed to in writing,
14
# software distributed under the License is distributed on an
15
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
# KIND, either express or implied. See the License for the
17
# specific language governing permissions and limitations
18
# under the License.
19
20
require 'selenium/webdriver/common/child_process'
21
require 'selenium/webdriver/common/port_prober'
22
require 'selenium/webdriver/common/socket_poller'
23
require 'net/http'
24
require 'json'
25
26
module Selenium
27
#
28
# Wraps the remote server jar
29
#
30
# Usage:
31
#
32
# server = Selenium::Server.new('/path/to/selenium-server-standalone.jar')
33
# server.start
34
#
35
# Automatically download the given version:
36
#
37
# server = Selenium::Server.get '2.6.0'
38
# server.start
39
#
40
# or the latest version:
41
#
42
# server = Selenium::Server.get :latest
43
# server.start
44
#
45
# Run the server in the background:
46
#
47
# server = Selenium::Server.new(jar, :background => true)
48
# server.start
49
#
50
# Add additional arguments:
51
#
52
# server = Selenium::Server.new(jar)
53
# server << ["--additional", "args"]
54
# server.start
55
#
56
57
class Server
58
class Error < StandardError; end
59
60
CL_RESET = WebDriver::Platform.windows? ? '' : "\r\e[0K"
61
62
class << self
63
#
64
# Download the given version of the selenium-server jar and return instance
65
#
66
# @param [String, Symbol] required_version X.Y.Z defaults to ':latest'
67
# @param [Hash] opts
68
# @return [Selenium::Server]
69
#
70
71
def get(required_version = :latest, opts = {})
72
new(download(required_version), opts)
73
end
74
75
#
76
# Download the given version of the selenium-server jar and return location
77
#
78
# @param [String, Symbol] required_version X.Y.Z defaults to ':latest'
79
# @return [String] location of downloaded file
80
#
81
82
def download(required_version = :latest)
83
required_version = latest if required_version == :latest
84
download_file_name = "selenium-server-#{required_version}.jar"
85
86
return download_file_name if File.exist? download_file_name
87
88
begin
89
download_location = available_assets[download_file_name]['browser_download_url']
90
released = Net::HTTP.get_response(URI.parse(download_location))
91
redirected = URI.parse released.header['location']
92
93
File.open(download_file_name, 'wb') do |destination|
94
download_server(redirected, destination)
95
end
96
rescue StandardError
97
FileUtils.rm_rf download_file_name
98
raise
99
end
100
101
download_file_name
102
end
103
104
#
105
# Ask GitHub what the latest selenium-server version is.
106
#
107
108
def latest
109
@latest ||= begin
110
available = available_assets.keys.map { |key| key[/selenium-server-(\d+\.\d+\.\d+)\.jar/, 1] }
111
available.map { |asset| Gem::Version.new(asset) }.max.to_s
112
end
113
end
114
115
# @api private
116
117
def available_assets
118
@available_assets ||= net_http_start('api.github.com') do |http|
119
json = http.get('/repos/seleniumhq/selenium/releases').body
120
all_assets = JSON.parse(json).map { |release| release['assets'] }.flatten
121
server_assets = all_assets.select { |asset| asset['name'].match(/selenium-server-(\d+\.\d+\.\d+)\.jar/) }
122
server_assets.each_with_object({}) { |asset, hash| hash[asset.delete('name')] = asset }
123
end
124
end
125
126
def net_http_start(address, &block)
127
http_proxy = ENV.fetch('http_proxy', nil) || ENV.fetch('HTTP_PROXY', nil)
128
if http_proxy
129
http_proxy = "http://#{http_proxy}" unless http_proxy.start_with?('http://')
130
uri = URI.parse(http_proxy)
131
132
Net::HTTP.start(address, nil, uri.host, uri.port, &block)
133
else
134
Net::HTTP.start(address, use_ssl: true, &block)
135
end
136
end
137
138
def download_server(uri, destination)
139
net_http_start('github-releases.githubusercontent.com') do |http|
140
request = Net::HTTP::Get.new uri
141
resp = http.request(request) do |response|
142
total = response.content_length
143
progress = 0
144
segment_count = 0
145
146
response.read_body do |segment|
147
progress += segment.length
148
segment_count += 1
149
150
if (segment_count % 15).zero?
151
percent = progress.fdiv(total) * 100
152
print "#{CL_RESET}Downloading #{destination.path}: #{percent.to_i}% (#{progress} / #{total})"
153
segment_count = 0
154
end
155
156
destination.write(segment)
157
end
158
end
159
160
raise Error, "#{resp.code} for #{destination.path}" unless resp.is_a? Net::HTTPSuccess
161
end
162
end
163
end
164
165
#
166
# The Mode of the Server
167
# :standalone, #hub, #node
168
#
169
170
attr_accessor :role, :host, :port, :timeout, :background, :log
171
172
#
173
# @param [String] jar Path to the server jar.
174
# @param [Hash] opts the options to create the server process with
175
#
176
# @option opts [Integer] :port Port the server should listen on (default: 4444).
177
# @option opts [Integer] :timeout Seconds to wait for server launch/shutdown (default: 30)
178
# @option opts [true,false] :background Run the server in the background (default: false)
179
# @option opts [true,false,String] :log Either a path to a log file,
180
# or true to pass server log to stdout.
181
# @raise [Errno::ENOENT] if the jar file does not exist
182
#
183
184
def initialize(jar, opts = {})
185
raise Errno::ENOENT, jar unless File.exist?(jar)
186
187
@java = opts.fetch(:java, 'java') || 'java'
188
@jar = jar
189
@host = '127.0.0.1'
190
@role = opts.fetch(:role, 'standalone')
191
@port = opts.fetch(:port, WebDriver::PortProber.above(4444))
192
@timeout = opts.fetch(:timeout, 30)
193
@background = opts.fetch(:background, false)
194
@additional_args = opts.fetch(:args, [])
195
@log = opts[:log]
196
if opts[:log_level]
197
@log ||= true
198
@additional_args << '--log-level'
199
@additional_args << opts[:log_level].to_s
200
end
201
202
@log_file = nil
203
end
204
205
def start
206
process.start
207
poll_for_ready
208
209
process.wait unless @background
210
end
211
212
def stop
213
stop_process if @process
214
poll_for_shutdown
215
216
@log_file&.close
217
end
218
219
def webdriver_url
220
"http://#{@host}:#{@port}/wd/hub"
221
end
222
223
def status_ok?
224
return false unless @process&.alive? && socket_connected?
225
226
Net::HTTP.start(@host, @port, open_timeout: 2, read_timeout: 2) do |http|
227
response = http.get('/status')
228
return false unless response.is_a?(Net::HTTPSuccess)
229
230
status = JSON.parse(response.body)
231
status.dig('value', 'ready') == true
232
end
233
rescue StandardError
234
false
235
end
236
237
def <<(arg)
238
if arg.is_a?(Array)
239
@additional_args += arg
240
else
241
@additional_args << arg.to_s
242
end
243
end
244
245
private
246
247
def stop_process
248
@process.stop
249
rescue Errno::ECHILD
250
# already dead
251
ensure
252
@process = nil
253
end
254
255
def process
256
@process ||= begin
257
# extract any additional_args that start with -D as options
258
properties = @additional_args.dup - @additional_args.delete_if { |arg| arg[/^-D/] }
259
args = ['-jar', @jar, @role, '--port', @port.to_s]
260
server_command = [@java] + properties + args + @additional_args
261
cp = WebDriver::ChildProcess.build(*server_command)
262
263
if @log.is_a?(String)
264
cp.io = @log
265
elsif @log
266
cp.io = :out
267
end
268
269
cp.detach = @background
270
271
cp
272
end
273
end
274
275
def poll_for_ready
276
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
277
loop do
278
return if status_ok?
279
280
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
281
raise Error, "remote server not ready in #{@timeout} seconds" if elapsed > @timeout
282
283
sleep 0.5
284
end
285
end
286
287
def socket_connected?
288
@socket_connected ||= socket.connected?
289
end
290
291
def poll_for_shutdown
292
return if socket.closed?
293
294
raise Error, "remote server not stopped in #{@timeout} seconds"
295
end
296
297
def socket
298
@socket ||= WebDriver::SocketPoller.new(@host, @port, @timeout)
299
end
300
end # Server
301
end # Selenium
302
303