class Headless
A class incapsulating the creation and usage of a headless X server
Prerequisites¶ ↑
-
X Window System
Usage¶ ↑
Block mode:
require 'rubygems' require 'headless' require 'selenium-webdriver' Headless.ly do driver = Selenium::WebDriver.for :firefox driver.navigate.to 'http://google.com' puts driver.title end
Object mode:
require 'rubygems' require 'headless' require 'selenium-webdriver' headless = Headless.new headless.start driver = Selenium::WebDriver.for :firefox driver.navigate.to 'http://google.com' puts driver.title headless.destroy
Constants
- DEFAULT_DISPLAY_DIMENSIONS
- DEFAULT_DISPLAY_NUMBER
- DEFAULT_XVFB_LAUNCH_TIMEOUT
- MAX_DISPLAY_NUMBER
Attributes
The display dimensions
The display number
Public Class Methods
Creates a new headless server, but does NOT switch to it immediately. Call start
for that
List of available options:
-
display
(default 99) - what display number to listen to; -
reuse
(default true) - if given display server already exists, should we use it or try another? -
autopick
(default true if display number isn't explicitly set) - ifHeadless
should automatically pick a display, or fail if the given one is not available. -
dimensions
(default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+. -
destroy_at_exit
- if a display is started but not stopped, should it be destroyed when the script finishes? (default true unless reuse is true and a server is already running) -
xvfb_launch_timeout
- how long should we wait for Xvfb to open a display, before assuming that it is frozen (in seconds, default is 10) -
video
- options to be passed to the ffmpeg video recorder. See Headless::VideoRecorder#initialize for documentation
# File lib/headless.rb, line 77 def initialize(options = {}) CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system') @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i @xvfb_launch_timeout = options.fetch(:xvfb_launch_timeout, DEFAULT_XVFB_LAUNCH_TIMEOUT).to_i @autopick_display = options.fetch(:autopick, !options.key?(:display)) @reuse_display = options.fetch(:reuse, true) @dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS) @video_capture_options = options.fetch(:video, {}) already_running = xvfb_running? rescue false @destroy_at_exit = options.fetch(:destroy_at_exit, !(@reuse_display && already_running)) @pid = nil # the pid of the running Xvfb process # FIXME Xvfb launch should not happen inside the constructor attach_xvfb end
Block syntax:
Headless.run do # perform operations in headless mode end
See new for options
# File lib/headless.rb, line 140 def self.run(options={}, &block) headless = Headless.new(options) headless.start yield headless ensure headless && headless.destroy end
Public Instance Methods
Switches back from the headless server and terminates the headless session while waiting for Xvfb process to terminate.
# File lib/headless.rb, line 110 def destroy stop CliUtil.kill_process(pid_filename, preserve_pid_file: true, wait: true) end
Whether the headless display will be destroyed when the script finishes.
# File lib/headless.rb, line 130 def destroy_at_exit? @destroy_at_exit end
Deprecated. Same as destroy. Kept for backward compatibility in June 2015.
# File lib/headless.rb, line 118 def destroy_sync destroy end
Same as the old destroy function – doesn't wait for Xvfb to die. Can cause zombies: stackoverflow.com/a/31003621/1651458
# File lib/headless.rb, line 124 def destroy_without_sync stop CliUtil.kill_process(pid_filename, preserve_pid_file: true) end
Switches to the headless server
# File lib/headless.rb, line 97 def start @old_display = ENV['DISPLAY'] ENV['DISPLAY'] = ":#{display}" hook_at_exit end
Switches back from the headless server
# File lib/headless.rb, line 104 def stop ENV['DISPLAY'] = @old_display end
# File lib/headless.rb, line 153 def take_screenshot(file_path, options={}) using = options.fetch(:using, :imagemagick) case using when :imagemagick CliUtil.ensure_application_exists!('import', "imagemagick is not found on your system. Please install it using sudo apt-get install imagemagick") system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}" when :xwd CliUtil.ensure_application_exists!('xwd', "xwd is not found on your system. Please install it using sudo apt-get install X11-apps") system "#{CliUtil.path_to('xwd')} -display localhost:#{display} -silent -root -out #{file_path}" when :graphicsmagick, :gm CliUtil.ensure_application_exists!('gm', "graphicsmagick is not found on your system. Please install it.") system "#{CliUtil.path_to('gm')} import -display localhost:#{display} -window root #{file_path}" else raise Headless::Exception.new('Unknown :using option value') end end
# File lib/headless.rb, line 149 def video @video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options) end
Private Instance Methods
# File lib/headless.rb, line 172 def attach_xvfb possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display) pick_available_display(possible_display_set, @reuse_display) end
# File lib/headless.rb, line 200 def ensure_xvfb_launched(out_pipe) start_time = Time.now errors = "" begin begin errors += out_pipe.read_nonblock(10000) if errors.include? "Cannot establish any listening sockets" raise Headless::Exception.new("Display socket is taken but lock file is missing - check the Headless troubleshooting guide") end if errors.include? "Server is already active for display #{display}" # This can happen if there is a race to grab the lock file. # Not an exception, just return false to let pick_available_display choose another: return false end rescue IO::WaitReadable # will retry next cycle end sleep 0.01 # to avoid cpu hogging raise Headless::Exception.new("Xvfb launched but did not complete initialization") if (Time.now-start_time)>=@xvfb_launch_timeout # Continue looping until Xvfb has written its pidfile: end while !xvfb_running? # If for any reason the pid file doesn't match ours, we lost the race to # get the file lock: return @pid == read_xvfb_pid end
# File lib/headless.rb, line 245 def hook_at_exit unless @at_exit_hook_installed @at_exit_hook_installed = true at_exit do exit_status = $!.status if $!.is_a?(SystemExit) destroy if destroy_at_exit? exit exit_status if exit_status end end end
# File lib/headless.rb, line 187 def launch_xvfb out_pipe, in_pipe = IO.pipe @pid = Process.spawn( CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac", err: in_pipe) raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless @pid # According to docs, you should either wait or detach on spawned procs: Process.detach @pid return ensure_xvfb_launched(out_pipe) ensure in_pipe.close end
# File lib/headless.rb, line 177 def pick_available_display(display_set, can_reuse) display_set.each do |display_number| @display = display_number return true if xvfb_running? && can_reuse && (xvfb_mine? || !@autopick_display) return true if !xvfb_running? && launch_xvfb end raise Headless::Exception.new("Could not find an available display") end
# File lib/headless.rb, line 237 def pid_filename "/tmp/.X#{display}-lock" end
# File lib/headless.rb, line 241 def read_xvfb_pid CliUtil.read_pid(pid_filename) end
# File lib/headless.rb, line 227 def xvfb_mine? CliUtil.process_mine?(read_xvfb_pid) end
Check whether an Xvfb process is running on @display. NOTE: This might be a process started by someone else!
# File lib/headless.rb, line 233 def xvfb_running? (pid = read_xvfb_pid) && CliUtil.process_running?(pid) end