Class: Tlopo::SeleniumDocker

Inherits:
Object
  • Object
show all
Defined in:
lib/tlopo/selenium_docker.rb,
lib/tlopo/selenium_docker/version.rb

Defined Under Namespace

Classes: NoFreePortError

Constant Summary collapse

VERSION =
"0.1.3"

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ SeleniumDocker

Returns a new instance of SeleniumDocker.



19
20
21
22
23
24
25
26
27
28
# File 'lib/tlopo/selenium_docker.rb', line 19

def initialize(opts = {})
  @selenium_image = opts[:selenium_image] || "selenium/standalone-chrome:4.8.3"
  @video_image = opts[:video_image] || "selenium/video:ffmpeg-4.3.1-20220726"
  @chrome_data_dir = opts[:chrome_data_dir] || nil
  @video_path = opts[:video_path] || nil
  @name = "selenium-#{gen_short_id}"
  @ws = "/tmp/#{@name}"
  FileUtils.mkdir_p @ws
  FileUtils.mkdir_p @chrome_data_dir if @chrome_data_dir
end

Instance Method Details

#best_effortObject



38
39
40
41
42
# File 'lib/tlopo/selenium_docker.rb', line 38

def best_effort
  yield
rescue StandardError
  nil
end

#copy_videoObject



78
79
80
81
82
83
84
# File 'lib/tlopo/selenium_docker.rb', line 78

def copy_video
  return unless @video_path

  path = @video_path =~ /\.mp4$/ ? @video_path : "#{@video_path}.mp4"
  FileUtils.cp "#{@ws}/video/video.mp4", path
  LOGGER.debug "Recorded video saved to '#{path}''"
end

#create_networkObject



106
107
108
109
110
111
112
# File 'lib/tlopo/selenium_docker.rb', line 106

def create_network
  network_name = @name.to_s
  return if Docker::Network.all.map(&:info).any? { |n| n["Name"] == network_name }

  LOGGER.debug "Creating network #{network_name}"
  Docker::Network.create network_name
end

#driverObject



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/tlopo/selenium_docker.rb', line 183

def driver
  return @driver unless @driver.nil?

  Timeout.timeout(60) do
    loop do
      TCPSocket.new("localhost", @port).close
      url = "http://localhost:#{@port}/wd/hub"

      options = Selenium::WebDriver::Chrome::Options.new
      options.add_argument("--no-first-run")
      options.add_argument("--no-sandbox")
      options.add_argument("--disable-dev-shm-usage")
      options.add_argument("--user-data-dir=/tmp/chrome-data-dir")

      @driver = Selenium::WebDriver.for :remote, url: url, capabilities: options
      LOGGER.debug "driver created for #{@name}"
      break
    rescue Errno::ECONNREFUSED
      sleep 0.5
    rescue EOFError
      sleep 0.5
    rescue Selenium::WebDriver::Error::UnknownError
      sleep 0.5
    end
  end
  @driver
end

#find_free_port(from, to) ⇒ Object

Raises:



86
87
88
89
90
91
92
93
94
95
# File 'lib/tlopo/selenium_docker.rb', line 86

def find_free_port(from, to)
  (from..to).each do |port|
    next if Tlopo::Futex.new("/tmp/.selenium-docker-#{port}").locked?

    Timeout.timeout(1) { TCPSocket.new("127.0.0.1", port).close }
  rescue Errno::ECONNREFUSED
    return port
  end
  raise NoFreePortError, "no free port available in range #{from}..#{to}"
end

#gen_short_idObject



44
45
46
# File 'lib/tlopo/selenium_docker.rb', line 44

def gen_short_id
  Zlib.crc32(Time.now.strftime("%s.%N")).to_s(36)
end

#get_container(container_name) ⇒ Object



122
123
124
125
126
# File 'lib/tlopo/selenium_docker.rb', line 122

def get_container(container_name)
  Docker::Container.get(container_name)
rescue Docker::Error::NotFoundError
  nil
end

#get_port_lock(port) ⇒ Object



48
49
50
51
52
53
# File 'lib/tlopo/selenium_docker.rb', line 48

def get_port_lock(port)
  filename = "/tmp/.selenium-docker-#{port}"
  f = Tlopo::Futex.new(filename)
  f.lock
  f
end

#lock_chrome_data_dirObject



136
137
138
139
# File 'lib/tlopo/selenium_docker.rb', line 136

def lock_chrome_data_dir
  @chrome_data_dir_lock = Tlopo::Futex.new("#{@chrome_data_dir}/.chrome_data_dir.lock")
  @chrome_data_dir_lock.lock
end

#pull_imagesObject



97
98
99
100
101
102
103
104
# File 'lib/tlopo/selenium_docker.rb', line 97

def pull_images
  [@selenium_image, @video_image].each do |image|
    unless Docker::Image.exist? image
      LOGGER.debug "Pulling image #{image}"
      Docker::Image.create("fromImage" => image)
    end
  end
end

#remove_networkObject



114
115
116
117
118
119
120
# File 'lib/tlopo/selenium_docker.rb', line 114

def remove_network
  network_name = @name.to_s
  return unless Docker::Network.all.map(&:info).any? { |n| n["Name"] == network_name }

  LOGGER.debug "Removing network #{network_name}"
  Docker::Network.get(network_name).remove
end

#runObject



30
31
32
33
34
35
36
# File 'lib/tlopo/selenium_docker.rb', line 30

def run
  start
  yield driver
ensure
  best_effort { driver.quit }
  stop
end

#startObject



55
56
57
58
59
60
61
62
63
64
65
# File 'lib/tlopo/selenium_docker.rb', line 55

def start
  pull_images
  lock_chrome_data_dir if @chrome_data_dir
  @port = find_free_port 8100, 8150
  @port_lock = get_port_lock @port
  @vnc_port = find_free_port 5900, 5950
  @vnc_port_lock = get_port_lock @vnc_port
  create_network
  start_selenium
  start_video if @video_path
end

#start_seleniumObject



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/tlopo/selenium_docker.rb', line 141

def start_selenium
  ws = @chrome_data_dir || "/#{@ws}/chrome-data-dir"
  LOGGER.debug "Starting selenium, container name: #{@name}, vnc port: #{@vnc_port}"
  Docker::Container.create(
    Image: @selenium_image,
    name: @name.to_s,
    HostConfig: {
      NetworkMode: @name.to_s,
      AutoRemove: true,
      PortBindings: {
        "4444/tcp": [{ HostPort: @port.to_s }],
        "5900/tcp": [{ HostPort: @vnc_port.to_s }]
      },
      "Binds" => ["#{ws}:/tmp/chrome-data-dir"],
      "ShmSize" => 2 * 1024 * 1024 * 1024 # 2 GB in bytes
    },
    Env: ["VIDEO=true"]
  ).start
end

#start_videoObject



165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/tlopo/selenium_docker.rb', line 165

def start_video
  LOGGER.debug "Starting video, container name: #{@name}-video"
  Docker::Container.create(
    Image: @video_image,
    name: "#{@name}-video",
    HostConfig: {
      NetworkMode: @name.to_s,
      AutoRemove: true,
      "Binds" => ["#{@ws}/video:/videos"]
    },
    Env: ["DISPLAY_CONTAINER_NAME=#{@name}"]
  ).start
end

#stopObject



67
68
69
70
71
72
73
74
75
76
# File 'lib/tlopo/selenium_docker.rb', line 67

def stop
  stop_video if @video_path
  stop_selenium
  remove_network
  @port_lock.release
  @vnc_port_lock.release
  @chrome_data_dir_lock&.release
  copy_video
  FileUtils.rm_rf @ws
end

#stop_container(container_name, timeout = 30) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/tlopo/selenium_docker.rb', line 128

def stop_container(container_name, timeout = 30)
  c = get_container(container_name)
  return if c.nil?

  LOGGER.debug "Stopping container '#{container_name}'"
  c.stop t: timeout
end

#stop_seleniumObject



161
162
163
# File 'lib/tlopo/selenium_docker.rb', line 161

def stop_selenium
  stop_container @name.to_s
end

#stop_videoObject



179
180
181
# File 'lib/tlopo/selenium_docker.rb', line 179

def stop_video
  stop_container "#{@name}-video"
end