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 ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - ree
7
+ script: "rspec"
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 a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
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
@@ -3,7 +3,7 @@ spec = Gem::Specification.new do |s|
3
3
  s.email = 'leonid@shevtsov.me'
4
4
 
5
5
  s.name = 'headless'
6
- s.version = '0.2.2'
6
+ s.version = '0.3.1'
7
7
  s.summary = 'Ruby headless display interface'
8
8
 
9
9
  s.description = <<-EOF
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 fail miserably?
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, 99).to_i
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, '1280x1024x24')
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
- #TODO more logic here, autopicking the display number
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
@@ -50,6 +50,8 @@ class Headless
50
50
  Process.wait pid if options[:wait]
51
51
  rescue Errno::ESRCH
52
52
  # no such process; assume it's already killed
53
+ rescue Errno::ECHILD
54
+ # Process.wait tried to wait on a dead process
53
55
  end
54
56
  end
55
57
 
@@ -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 qtrle #{@tmp_file_path}", @pid_file_path, @log_file_path)
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
 
@@ -1,4 +1,4 @@
1
- require 'lib/headless'
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 "instaniation" do
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
- it "raises an error if reuse display is not allowed" do
40
- lambda { Headless.new(:reuse => false) }.should raise_error(Headless::Exception)
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
- it "doesn't raise an error if reuse display is allowed" do
44
- lambda { Headless.new(:reuse => true) }.should_not raise_error(Headless::Exception)
45
- lambda { Headless.new }.should_not raise_error(Headless::Exception)
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
@@ -1,11 +1,11 @@
1
- require 'lib/headless'
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 "instaniation" do
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 => "/tmp/pid", :tmp_file_path => "/tmp/ci.mov")
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("/tmp/pid", :wait => true)
38
- FileUtils.should_receive(:mv).with("/tmp/ci.mov", "/tmp/test.mov")
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("/tmp/test.mov")
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("/tmp/pid", :wait => true)
47
- FileUtils.should_receive(:rm).with("/tmp/ci.mov")
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
- hash: 19
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
- date: 2011-09-01 00:00:00 +03:00
19
- default_executable:
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
- prerelease: false
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
- hash: 15
30
- segments:
31
- - 2
32
- - 6
33
- version: "2.6"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.6'
34
22
  type: :development
35
- version_requirements: *id001
36
- description: " Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.\n"
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
- hash: 3
72
- segments:
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
- hash: 3
81
- segments:
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.6.2
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
-