Class: Tlopo::SeleniumDocker

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

Constant Summary collapse

VERSION =
"0.1.2"

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ SeleniumDocker

Returns a new instance of SeleniumDocker.



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

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



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

def best_effort
  begin
    yield
  rescue
  end
end

#copy_videoObject



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

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



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

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



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

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("--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



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

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
end

#gen_short_idObject



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

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

#get_container(container_name) ⇒ Object



120
121
122
123
124
# File 'lib/tlopo/selenium_docker.rb', line 120

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

#get_port_lock(port) ⇒ Object



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

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

#lock_chrome_data_dirObject



134
135
136
137
# File 'lib/tlopo/selenium_docker.rb', line 134

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



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

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



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

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



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

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

#startObject



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

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



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

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



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

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



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

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



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

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



159
160
161
# File 'lib/tlopo/selenium_docker.rb', line 159

def stop_selenium
  stop_container @name.to_s
end

#stop_videoObject



177
178
179
# File 'lib/tlopo/selenium_docker.rb', line 177

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