headless 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -0
- data/CHANGELOG +5 -0
- data/README.md +13 -2
- data/headless.gemspec +1 -1
- data/lib/headless.rb +40 -11
- data/lib/headless/cli_util.rb +2 -0
- data/lib/headless/video/video_recorder.rb +4 -1
- data/spec/headless_spec.rb +55 -8
- data/spec/video_recorder_spec.rb +21 -8
- metadata +32 -52
data/.travis.yml
ADDED
data/CHANGELOG
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## 0.3.1 (2012-03-29)
|
2
|
+
|
3
|
+
* added autopicking of display number, if the requested one is already taken
|
4
|
+
* fixed plenty of bugs thanks to @recursive, @gshakhn, @masatomo and @mabotelh
|
5
|
+
|
1
6
|
## 0.2.2 (2011-09-01)
|
2
7
|
|
3
8
|
* improve detection of ffmpeg process (from https://github.com/alanshields/headless)
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# Headless
|
1
|
+
# Headless [![Travis CI status](https://secure.travis-ci.org/leonid-shevtsov/headless.png)](http://travis-ci.org/leonid-shevtsov/headless)
|
2
2
|
|
3
|
-
Headless is
|
3
|
+
Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
|
4
4
|
It can also capture images and video from the virtual framebuffer.
|
5
5
|
|
6
6
|
I created it so I can run Selenium tests in Cucumber without any shell scripting. Even more, you can go headless only when you run tests against Selenium.
|
@@ -8,6 +8,8 @@ Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting
|
|
8
8
|
|
9
9
|
Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
|
10
10
|
|
11
|
+
[Changelog](https://github.com/leonid-shevtsov/headless/blob/master/CHANGELOG)
|
12
|
+
|
11
13
|
## Installation
|
12
14
|
|
13
15
|
On Debian/Ubuntu:
|
@@ -57,6 +59,15 @@ Running cucumber headless is now as simple as adding a before and after hook in
|
|
57
59
|
headless.start
|
58
60
|
end
|
59
61
|
|
62
|
+
## Cucumber with wkhtmltopdf
|
63
|
+
|
64
|
+
_Note: this is true for other programs which may use headless at the same time as cucumber is running_
|
65
|
+
|
66
|
+
When wkhtmltopdf is using Headless, and cucumber is invoking a block of code which uses a headless session, make sure to override the default display of cucumber to retain browser focus. Assuming wkhtmltopdf is using the default display of 99, make sure to set the display to a value != 99 in `features/support/env.rb` file. This may be the cause of `Connection refused - connect(2) (Errno::ECONNREFUSED)`.
|
67
|
+
|
68
|
+
headless = Headless.new(:display => '100')
|
69
|
+
headless.start
|
70
|
+
|
60
71
|
## Capturing video
|
61
72
|
|
62
73
|
Video is captured using `ffmpeg`. You can install it on Debian/Ubuntu via `sudo apt-get install ffmpeg` or on OS X via `brew install ffmpeg`. You can capture video continuously or capture scenarios separately. Here is typical use case:
|
data/headless.gemspec
CHANGED
data/lib/headless.rb
CHANGED
@@ -41,6 +41,9 @@ require 'headless/video/video_recorder'
|
|
41
41
|
#++
|
42
42
|
class Headless
|
43
43
|
|
44
|
+
DEFAULT_DISPLAY_NUMBER = 99
|
45
|
+
DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
|
46
|
+
|
44
47
|
class Exception < RuntimeError
|
45
48
|
end
|
46
49
|
|
@@ -54,26 +57,21 @@ class Headless
|
|
54
57
|
#
|
55
58
|
# List of available options:
|
56
59
|
# * +display+ (default 99) - what display number to listen to;
|
57
|
-
# * +reuse+ (default true) - if given display server already exists, should we use it or
|
60
|
+
# * +reuse+ (default true) - if given display server already exists, should we use it or try another?
|
61
|
+
# * +autopick+ (default true is display number isn't explicitly set) - if Headless should automatically pick a display, or fail if the given one is not available.
|
58
62
|
# * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
|
59
63
|
# * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
|
60
64
|
def initialize(options = {})
|
61
65
|
CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
|
62
66
|
|
63
|
-
@display = options.fetch(:display,
|
67
|
+
@display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
|
68
|
+
@autopick_display = options.fetch(:autopick, !options.key?(:display))
|
64
69
|
@reuse_display = options.fetch(:reuse, true)
|
65
|
-
@dimensions = options.fetch(:dimensions,
|
70
|
+
@dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
|
66
71
|
@video_capture_options = options.fetch(:video, {})
|
67
72
|
@destroy_at_exit = options.fetch(:destroy_at_exit, true)
|
68
73
|
|
69
|
-
|
70
|
-
if @reuse_display
|
71
|
-
launch_xvfb unless xvfb_running?
|
72
|
-
elsif xvfb_running?
|
73
|
-
raise Headless::Exception.new("Display :#{display} is already taken and reuse=false")
|
74
|
-
else
|
75
|
-
launch_xvfb
|
76
|
-
end
|
74
|
+
attach_xvfb
|
77
75
|
end
|
78
76
|
|
79
77
|
# Switches to the headless server
|
@@ -120,6 +118,35 @@ class Headless
|
|
120
118
|
|
121
119
|
private
|
122
120
|
|
121
|
+
def attach_xvfb
|
122
|
+
# TODO this loop isn't elegant enough
|
123
|
+
success = false
|
124
|
+
while !success && @display<10000
|
125
|
+
begin
|
126
|
+
if !xvfb_running?
|
127
|
+
launch_xvfb
|
128
|
+
success=true
|
129
|
+
else
|
130
|
+
success = @reuse_display
|
131
|
+
end
|
132
|
+
rescue Errno::EPERM
|
133
|
+
# No permission to read pid file
|
134
|
+
success = false
|
135
|
+
end
|
136
|
+
|
137
|
+
# TODO this is crufty
|
138
|
+
if @autopick_display
|
139
|
+
@display += 1 unless success
|
140
|
+
else
|
141
|
+
break
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
unless success
|
146
|
+
raise Headless::Exception.new("Display :#{display} is already taken and reuse=false")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
123
150
|
def launch_xvfb
|
124
151
|
#TODO error reporting
|
125
152
|
result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
|
@@ -142,7 +169,9 @@ private
|
|
142
169
|
unless @at_exit_hook_installed
|
143
170
|
@at_exit_hook_installed = true
|
144
171
|
at_exit do
|
172
|
+
exit_status = $!.status if $!.is_a?(SystemExit)
|
145
173
|
destroy if @destroy_at_exit
|
174
|
+
exit exit_status if exit_status
|
146
175
|
end
|
147
176
|
end
|
148
177
|
end
|
data/lib/headless/cli_util.rb
CHANGED
@@ -13,6 +13,7 @@ class Headless
|
|
13
13
|
@pid_file_path = options.fetch(:pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid")
|
14
14
|
@tmp_file_path = options.fetch(:tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov")
|
15
15
|
@log_file_path = options.fetch(:log_file_path, "/dev/null")
|
16
|
+
@codec = options.fetch(:codec, "qtrle")
|
16
17
|
end
|
17
18
|
|
18
19
|
def capture_running?
|
@@ -20,9 +21,11 @@ class Headless
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def start_capture
|
23
|
-
CliUtil.fork_process("#{CliUtil.path_to('ffmpeg')} -y -r 30 -g 600 -s #{@dimensions} -f x11grab -i :#{@display} -vcodec
|
24
|
+
CliUtil.fork_process("#{CliUtil.path_to('ffmpeg')} -y -r 30 -g 600 -s #{@dimensions} -f x11grab -i :#{@display} -vcodec #{@codec} #{@tmp_file_path}", @pid_file_path, @log_file_path)
|
24
25
|
at_exit do
|
26
|
+
exit_status = $!.status if $!.is_a?(SystemExit)
|
25
27
|
stop_and_discard
|
28
|
+
exit exit_status if exit_status
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
data/spec/headless_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'headless'
|
2
2
|
|
3
3
|
describe Headless do
|
4
4
|
before do
|
@@ -6,7 +6,7 @@ describe Headless do
|
|
6
6
|
stub_environment
|
7
7
|
end
|
8
8
|
|
9
|
-
context "
|
9
|
+
context "instantiation" do
|
10
10
|
context "when Xvfb is not installed" do
|
11
11
|
before do
|
12
12
|
Headless::CliUtil.stub!(:application_exists?).and_return(false)
|
@@ -33,16 +33,63 @@ describe Headless do
|
|
33
33
|
|
34
34
|
context "when Xvfb is already running" do
|
35
35
|
before do
|
36
|
-
Headless::CliUtil.stub!(:read_pid).and_return(31337)
|
36
|
+
Headless::CliUtil.stub!(:read_pid).with('/tmp/.X99-lock').and_return(31337)
|
37
|
+
Headless::CliUtil.stub!(:read_pid).with('/tmp/.X100-lock').and_return(nil)
|
37
38
|
end
|
38
39
|
|
39
|
-
|
40
|
-
|
40
|
+
context "and display reuse is allowed" do
|
41
|
+
let(:options) { {:reuse => true} }
|
42
|
+
|
43
|
+
it "should reuse the existing Xvfb" do
|
44
|
+
Headless.new(options).display.should == 99
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "and display reuse is not allowed" do
|
49
|
+
let(:options) { {:reuse => false} }
|
50
|
+
|
51
|
+
it "should pick the next available display number" do
|
52
|
+
Headless.new(options).display.should == 100
|
53
|
+
end
|
54
|
+
|
55
|
+
context "and display number is explicitly set" do
|
56
|
+
let(:options) { {:reuse => false, :display => 99} }
|
57
|
+
|
58
|
+
it "should fail with an exception" do
|
59
|
+
lambda { Headless.new(options) }.should raise_error(Headless::Exception)
|
60
|
+
end
|
61
|
+
|
62
|
+
context "and autopicking is allowed" do
|
63
|
+
let(:options) { {:reuse => false, :display => 99, :autopick => true} }
|
64
|
+
|
65
|
+
it "should pick the next available display number" do
|
66
|
+
Headless.new(options).display.should == 100
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when Xvfb is started, but by another user' do
|
74
|
+
before do
|
75
|
+
Headless::CliUtil.stub!(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
|
76
|
+
Headless::CliUtil.stub!(:read_pid).with('/tmp/.X100-lock').and_return(nil)
|
41
77
|
end
|
42
78
|
|
43
|
-
|
44
|
-
|
45
|
-
|
79
|
+
context "and display autopicking is not allowed" do
|
80
|
+
let(:options) { {:autopick => false} }
|
81
|
+
|
82
|
+
it "should fail with and exception" do
|
83
|
+
lambda { Headless.new(options) }.should raise_error(Headless::Exception)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "and display autopicking is allowed" do
|
88
|
+
let(:options) { {:autopick => true} }
|
89
|
+
|
90
|
+
it "should pick the next display number" do
|
91
|
+
Headless.new(options).display.should == 100
|
92
|
+
end
|
46
93
|
end
|
47
94
|
end
|
48
95
|
end
|
data/spec/video_recorder_spec.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
require '
|
1
|
+
require 'headless'
|
2
2
|
|
3
3
|
describe Headless::VideoRecorder do
|
4
4
|
before do
|
5
5
|
stub_environment
|
6
6
|
end
|
7
7
|
|
8
|
-
describe "
|
8
|
+
describe "instantiation" do
|
9
9
|
before do
|
10
10
|
Headless::CliUtil.stub!(:application_exists?).and_return(false)
|
11
11
|
end
|
@@ -23,28 +23,41 @@ describe Headless::VideoRecorder do
|
|
23
23
|
recorder = Headless::VideoRecorder.new(99, "1024x768x32")
|
24
24
|
recorder.start_capture
|
25
25
|
end
|
26
|
+
|
27
|
+
it "starts ffmpeg with specified codec" do
|
28
|
+
Headless::CliUtil.stub(:path_to, 'ffmpeg').and_return('ffmpeg')
|
29
|
+
Headless::CliUtil.should_receive(:fork_process).with(/ffmpeg -y -r 30 -g 600 -s 1024x768x32 -f x11grab -i :99 -vcodec libvpx/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
|
30
|
+
|
31
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'})
|
32
|
+
recorder.start_capture
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
context "stopping video recording" do
|
37
|
+
let(:tmpfile) { '/tmp/ci.mov' }
|
38
|
+
let(:filename) { '/tmp/test.mov' }
|
39
|
+
let(:pidfile) { '/tmp/pid' }
|
40
|
+
|
29
41
|
subject do
|
30
|
-
recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path =>
|
42
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile)
|
31
43
|
recorder.start_capture
|
32
44
|
recorder
|
33
45
|
end
|
34
46
|
|
35
47
|
describe "using #stop_and_save" do
|
36
48
|
it "stops video recording and saves file" do
|
37
|
-
Headless::CliUtil.should_receive(:kill_process).with(
|
38
|
-
|
49
|
+
Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
|
50
|
+
File.should_receive(:exists?).with(tmpfile).and_return(true)
|
51
|
+
FileUtils.should_receive(:mv).with(tmpfile, filename)
|
39
52
|
|
40
|
-
subject.stop_and_save(
|
53
|
+
subject.stop_and_save(filename)
|
41
54
|
end
|
42
55
|
end
|
43
56
|
|
44
57
|
describe "using #stop_and_discard" do
|
45
58
|
it "stops video recording and deletes temporary file" do
|
46
|
-
Headless::CliUtil.should_receive(:kill_process).with(
|
47
|
-
FileUtils.should_receive(:rm).with(
|
59
|
+
Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
|
60
|
+
FileUtils.should_receive(:rm).with(tmpfile)
|
48
61
|
|
49
62
|
subject.stop_and_discard
|
50
63
|
end
|
metadata
CHANGED
@@ -1,48 +1,38 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: headless
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 2
|
9
|
-
- 2
|
10
|
-
version: 0.2.2
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- Leonid Shevtsov
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2012-03-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
22
15
|
name: rspec
|
23
|
-
|
24
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: &70170211681540 !ruby/object:Gem::Requirement
|
25
17
|
none: false
|
26
|
-
requirements:
|
18
|
+
requirements:
|
27
19
|
- - ~>
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
|
30
|
-
segments:
|
31
|
-
- 2
|
32
|
-
- 6
|
33
|
-
version: "2.6"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.6'
|
34
22
|
type: :development
|
35
|
-
|
36
|
-
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70170211681540
|
25
|
+
description: ! ' Headless is a Ruby interface for Xvfb. It allows you to create
|
26
|
+
a headless display straight from Ruby code, hiding some low-level action.
|
27
|
+
|
28
|
+
'
|
37
29
|
email: leonid@shevtsov.me
|
38
30
|
executables: []
|
39
|
-
|
40
31
|
extensions: []
|
41
|
-
|
42
32
|
extra_rdoc_files: []
|
43
|
-
|
44
|
-
files:
|
33
|
+
files:
|
45
34
|
- .gitignore
|
35
|
+
- .travis.yml
|
46
36
|
- CHANGELOG
|
47
37
|
- Gemfile
|
48
38
|
- LICENSE
|
@@ -54,39 +44,29 @@ files:
|
|
54
44
|
- lib/headless/video/video_recorder.rb
|
55
45
|
- spec/headless_spec.rb
|
56
46
|
- spec/video_recorder_spec.rb
|
57
|
-
has_rdoc: true
|
58
47
|
homepage: http://leonid.shevtsov.me/en/headless
|
59
48
|
licenses: []
|
60
|
-
|
61
49
|
post_install_message:
|
62
50
|
rdoc_options: []
|
63
|
-
|
64
|
-
require_paths:
|
51
|
+
require_paths:
|
65
52
|
- lib
|
66
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
54
|
none: false
|
68
|
-
requirements:
|
69
|
-
- -
|
70
|
-
- !ruby/object:Gem::Version
|
71
|
-
|
72
|
-
|
73
|
-
- 0
|
74
|
-
version: "0"
|
75
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
60
|
none: false
|
77
|
-
requirements:
|
78
|
-
- -
|
79
|
-
- !ruby/object:Gem::Version
|
80
|
-
|
81
|
-
|
82
|
-
- 0
|
83
|
-
version: "0"
|
84
|
-
requirements:
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements:
|
85
66
|
- Xvfb
|
86
67
|
rubyforge_project:
|
87
|
-
rubygems_version: 1.
|
68
|
+
rubygems_version: 1.8.17
|
88
69
|
signing_key:
|
89
70
|
specification_version: 3
|
90
71
|
summary: Ruby headless display interface
|
91
72
|
test_files: []
|
92
|
-
|