Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Test

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby:
- "3.4"
- "3.3"
- "3.2"
- "jruby-10.0.0.1"
- "jruby-9.4.12.1"
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg imagemagick
- name: Install geckodriver
env:
GECKODRIVER_VERSION: v0.36.0
run: |
wget https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz
mkdir -p geckodriver
tar -xzf geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -C geckodriver
echo "$GITHUB_WORKSPACE/geckodriver" >> $GITHUB_PATH
- name: Run tests
run: bundle exec rspec
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
.bundle
Gemfile.lock
pkg/*
20 changes: 0 additions & 20 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source "http://rubygems.org"

# Specify your gem's dependencies in ci_util.gemspec
# Specify dependencies in headless.gemspec
gemspec
96 changes: 96 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
PATH
remote: .
specs:
headless (2.3.1)

GEM
remote: http://rubygems.org/
specs:
ast (2.4.3)
base64 (0.2.0)
diff-lcs (1.6.1)
json (2.11.3)
json (2.11.3-java)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.7.0)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
prism (1.4.0)
racc (1.8.1)
racc (1.8.1-java)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.10.0)
rexml (3.4.1)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.3)
rubocop (1.75.5)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (1.13.0)
rubyzip (2.4.1)
selenium-webdriver (4.32.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
standard (1.49.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.75.2)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.8.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.25.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
websocket (1.2.11)

PLATFORMS
java
ruby

DEPENDENCIES
headless!
rake
rspec (>= 3.7)
selenium-webdriver (>= 4.32)
standard

BUNDLED WITH
2.6.8
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,9 @@ Available options:

* :codec - codec to be used by ffmpeg
* :frame_rate - frame rate of video capture
* :provider - ffmpeg provider - either :libav (default) or :ffmpeg
* :provider_binary_path - Explicit path to avconv or ffmpeg. Only required when the binary cannot be discovered on the system $PATH.
* :pid*file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg*#{@display}.pid"
* :tmp*file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg*#{@display}.mov"
* :ffmpeg_path - Explicit path to ffmpeg. Only required when the binary cannot be discovered on the system $PATH.
* :pid_file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg_#{@display}.pid"
* :tmp_file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg_#{@display}.mov"
* :log_file_path - ffmpeg log file, default: "/dev/null"
* :extra - array of extra ffmpeg options, default: []

Expand Down
25 changes: 13 additions & 12 deletions headless.gemspec
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
Gem::Specification.new do |s|
s.author = 'Leonid Shevtsov'
s.email = 'leonid@shevtsov.me'
s.author = "Leonid Shevtsov"
s.email = "leonid@shevtsov.me"

s.name = 'headless'
s.version = '2.3.1'
s.summary = 'Ruby headless display interface'
s.license = 'MIT'
s.name = "headless"
s.version = "2.3.1"
s.summary = "Ruby headless display interface"
s.license = "MIT"

s.description = <<-EOF
Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
EOF
s.requirements = 'Xvfb'
s.homepage = 'https://github.com/leonid-shevtsov/headless'
s.requirements = "Xvfb"
s.homepage = "https://github.com/leonid-shevtsov/headless"

s.files = `git ls-files`.split("\n")
s.files = `git ls-files`.split("\n")

s.add_development_dependency 'rake'
s.add_development_dependency 'rspec', '>= 3.7'
s.add_development_dependency 'selenium-webdriver', '>=3.7'
s.add_development_dependency "rake"
s.add_development_dependency "rspec", ">= 3.7"
s.add_development_dependency "selenium-webdriver", ">=4.32"
s.add_development_dependency "standard"
end
81 changes: 48 additions & 33 deletions lib/headless.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require 'headless/cli_util'
require 'headless/video/video_recorder'
require "headless/cli_util"
require "headless/video/video_recorder"

# A class incapsulating the creation and usage of a headless X server
#
Expand Down Expand Up @@ -40,10 +40,9 @@
# TODO test that reuse actually works with an existing xvfb session
#++
class Headless

DEFAULT_DISPLAY_NUMBER = 99
MAX_DISPLAY_NUMBER = 10_000
DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
DEFAULT_DISPLAY_DIMENSIONS = "1280x1024x24"
DEFAULT_XVFB_LAUNCH_TIMEOUT = 10

class Exception < RuntimeError
Expand Down Expand Up @@ -73,9 +72,10 @@ class Exception < RuntimeError
# (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
# * +video+ - options to be passed to the ffmpeg video recorder.
# See Headless::VideoRecorder#initialize for documentation
def initialize(options = {})
CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
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
Expand All @@ -86,7 +86,11 @@ def initialize(options = {})
@extensions = options.fetch(:extensions, [])
@extensions = [@extensions] unless @extensions.is_a? Array

already_running = xvfb_running? rescue false
already_running = begin
xvfb_running?
rescue
false
end
@destroy_at_exit = options.fetch(:destroy_at_exit, !(@reuse_display && already_running))

@pid = nil # the pid of the running Xvfb process
Expand All @@ -97,14 +101,14 @@ def initialize(options = {})

# Switches to the headless server
def start
@old_display = ENV['DISPLAY']
ENV['DISPLAY'] = ":#{display}"
@old_display = ENV["DISPLAY"]
ENV["DISPLAY"] = ":#{display}"
hook_at_exit
end

# Switches back from the headless server
def stop
ENV['DISPLAY'] = @old_display
ENV["DISPLAY"] = @old_display
end

# Switches back from the headless server and terminates the headless session
Expand Down Expand Up @@ -139,37 +143,42 @@ def destroy_at_exit?
# # perform operations in headless mode
# end
# See #new for options
def self.run(options={}, &block)
def self.run(options = {}, &block)
headless = Headless.new(options)
headless.start
yield headless
ensure
headless && headless.destroy
headless&.destroy
end
class <<self; alias_method :ly, :run; end
class << self; alias_method :ly, :run; end

def video
@video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
end

def take_screenshot(file_path, options={})
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 :#{display} -window root #{file_path}"
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 :#{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}"
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}"
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')
raise Headless::Exception.new("Unknown :using option value")
end
end

private
private

def attach_xvfb
possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display)
Expand All @@ -190,25 +199,28 @@ def launch_xvfb
out_pipe, in_pipe = IO.pipe
@pid = Process.spawn(
CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac", *extensions,
err: in_pipe)
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
ensure_xvfb_launched(out_pipe)
ensure
in_pipe.close
end

def ensure_xvfb_launched(out_pipe)
start_time = Time.now
errors = ""
begin
loop do
begin
errors += out_pipe.read_nonblock(10000)
if errors.include? "directory /tmp/.X11-unix will not be created."
raise Headless::Exception, "/tmp/.X11-unix is missing - check the Headless troubleshooting guide"
elsif errors.include? "Cannot establish any listening sockets"
raise Headless::Exception, "Display socket is taken but lock file is missing - check the Headless troubleshooting guide"
raise Headless::Exception,
"Display socket is taken but lock file is missing - check the Headless troubleshooting guide"
elsif 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:
Expand All @@ -218,13 +230,16 @@ def ensure_xvfb_launched(out_pipe)
# 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 (Time.now - start_time) >= @xvfb_launch_timeout
raise Headless::Exception.new("Xvfb launched but did not complete initialization")
end
# Continue looping until Xvfb has written its pidfile:
break if xvfb_running?
end

# 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
@pid == read_xvfb_pid
end

def xvfb_mine?
Expand Down Expand Up @@ -257,6 +272,6 @@ def hook_at_exit
end

def extensions
@extensions.map { |ext| '+' + ext.to_s }
@extensions.map { |ext| "+" + ext.to_s }
end
end
Loading
Loading