headless 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
- ## 0.1.0
1
+ ## 0.2.1 (2011-08-26)
2
+ * added ability to capture screenshots (from https://github.com/iafonov/headless)
3
+ * added ability to capture video (from https://github.com/iafonov/headless)
4
+ * fixed issue with stray pidfile
5
+
6
+ ## 0.1.0 (2010-08-15)
2
7
  * introduced options
3
8
  * make it possible to change virtual screen dimensions and pixel depth
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ci_util.gemspec
4
+ gemspec
data/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  # Headless
2
2
 
3
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.
4
+ It can also capture images and video from the virtual framebuffer.
4
5
 
5
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.
6
7
  Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
7
8
 
9
+ Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
10
+
8
11
  ## Installation
9
12
 
10
13
  On Debian/Ubuntu:
@@ -52,12 +55,37 @@ Running cucumber headless is now as simple as adding a before and after hook in
52
55
 
53
56
  headless = Headless.new
54
57
  headless.start
58
+ end
59
+
60
+ ## Capturing video
61
+
62
+ 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:
55
63
 
56
- at_exit do
57
- headless.destroy
64
+ require 'headless'
65
+
66
+ headless = Headless.new
67
+ headless.start
68
+
69
+ Before do
70
+ headless.video.start_capture
71
+ end
72
+
73
+ After do |scenario|
74
+ if scenario.failed?
75
+ headless.video.stop_and_save("/tmp/#{BUILD_ID}/#{scenario.name.split.join("_")}.mov")
76
+ else
77
+ headless.video.stop_and_discard
58
78
  end
59
79
  end
60
80
 
81
+ ## Taking screenshots
82
+
83
+ Images are captured using `import` utility which is part of `imagemagick` library. You can install it on Ubuntu via `sudo apt-get install imagemagick`. You can call `headless.take_screenshot` at any time. You have to supply full path to target file. File format is determined by supplied file extension.
84
+
85
+ ## Contributors
86
+
87
+ * [Igor Afonov](http://iafonov.github.com) - video and screenshot capturing functionality.
88
+
61
89
  ---
62
90
 
63
- © 2010 Leonid Shevtsov, released under the MIT license
91
+ © 2011 Leonid Shevtsov, released under the MIT license
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.author = 'Leonid Shevtsov'
3
+ s.email = 'leonid@shevtsov.me'
4
+
5
+ s.name = 'headless'
6
+ s.version = '0.2.1'
7
+ s.summary = 'Ruby headless display interface'
8
+
9
+ s.description = <<-EOF
10
+ Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
11
+ EOF
12
+ s.requirements = 'Xvfb'
13
+ s.homepage = 'http://leonid.shevtsov.me/en/headless'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+
17
+ s.add_development_dependency "rspec", "~> 2.6"
18
+ end
@@ -1,3 +1,6 @@
1
+ require 'headless/cli_util'
2
+ require 'headless/video/video_recorder'
3
+
1
4
  # A class incapsulating the creation and usage of a headless X server
2
5
  #
3
6
  # == Prerequisites
@@ -38,7 +41,7 @@
38
41
  #++
39
42
  class Headless
40
43
 
41
- class Exception < ::Exception
44
+ class Exception < RuntimeError
42
45
  end
43
46
 
44
47
  # The display number
@@ -53,29 +56,31 @@ class Headless
53
56
  # * +display+ (default 99) - what display number to listen to;
54
57
  # * +reuse+ (default true) - if given display server already exists, should we use it or fail miserably?
55
58
  # * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
59
+ # * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
56
60
  def initialize(options = {})
57
- find_xvfb
61
+ CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
58
62
 
59
63
  @display = options.fetch(:display, 99).to_i
60
64
  @reuse_display = options.fetch(:reuse, true)
61
65
  @dimensions = options.fetch(:dimensions, '1280x1024x24')
66
+ @video_capture_options = options.fetch(:video, {})
67
+ @destroy_at_exit = options.fetch(:destroy_at_exit, true)
62
68
 
63
69
  #TODO more logic here, autopicking the display number
64
70
  if @reuse_display
65
- launch_xvfb unless read_pid
66
- elsif read_pid
67
- raise Exception.new("Display :#{display} is already taken and reuse=false")
71
+ launch_xvfb unless xvfb_running?
72
+ elsif xvfb_running?
73
+ raise Headless::Exception.new("Display :#{display} is already taken and reuse=false")
68
74
  else
69
75
  launch_xvfb
70
76
  end
71
-
72
- raise Exception.new("Xvfb did not launch - something's wrong") unless read_pid
73
77
  end
74
78
 
75
79
  # Switches to the headless server
76
80
  def start
77
81
  @old_display = ENV['DISPLAY']
78
82
  ENV['DISPLAY'] = ":#{display}"
83
+ hook_at_exit
79
84
  end
80
85
 
81
86
  # Switches back from the headless server
@@ -86,7 +91,7 @@ class Headless
86
91
  # Switches back from the headless server and terminates the headless session
87
92
  def destroy
88
93
  stop
89
- Process.kill('TERM', xvfb_pid) if read_pid
94
+ CliUtil.kill_process(pid_filename)
90
95
  end
91
96
 
92
97
  # Block syntax:
@@ -101,27 +106,44 @@ class Headless
101
106
  yield headless
102
107
  headless.destroy
103
108
  end
104
-
105
109
  class <<self; alias_method :ly, :run; end
106
110
 
107
- private
108
- attr_reader :xvfb_pid
111
+ def video
112
+ @video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
113
+ end
114
+
115
+ def take_screenshot(file_path)
116
+ CliUtil.ensure_application_exists!('import', "imagemagick not found on your system. Please install it using sudo apt-get install imagemagick")
109
117
 
110
- def find_xvfb
111
- @xvfb = `which Xvfb`.strip
112
- raise Exception.new('Xvfb not found on your system') if @xvfb == ''
118
+ system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}"
113
119
  end
114
120
 
121
+ private
122
+
115
123
  def launch_xvfb
116
124
  #TODO error reporting
117
- system "#{@xvfb} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
118
- sleep 1
125
+ result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
126
+ raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless result
127
+ end
128
+
129
+ def xvfb_running?
130
+ !!read_xvfb_pid
119
131
  end
120
132
 
121
- def read_pid
122
- @xvfb_pid=(File.read("/tmp/.X#{display}-lock") rescue "").strip.to_i
123
- @xvfb_pid=nil if @xvfb_pid==0
124
- @xvfb_pid
125
- #TODO maybe check that the process still exists
133
+ def pid_filename
134
+ "/tmp/.X#{display}-lock"
135
+ end
136
+
137
+ def read_xvfb_pid
138
+ CliUtil.read_pid(pid_filename)
139
+ end
140
+
141
+ def hook_at_exit
142
+ unless @at_exit_hook_installed
143
+ @at_exit_hook_installed = true
144
+ at_exit do
145
+ destroy if @destroy_at_exit
146
+ end
147
+ end
126
148
  end
127
149
  end
@@ -0,0 +1,52 @@
1
+ class Headless
2
+ class CliUtil
3
+ def self.application_exists?(app)
4
+ `which #{app}`.strip != ""
5
+ end
6
+
7
+ def self.ensure_application_exists!(app, error_message)
8
+ if !self.application_exists?(app)
9
+ raise Headless::Exception.new(error_message)
10
+ end
11
+ end
12
+
13
+ def self.path_to(app)
14
+ `which #{app}`.strip
15
+ end
16
+
17
+ def self.read_pid(pid_filename)
18
+ pid = (File.read(pid_filename) rescue "").strip.to_i
19
+ pid == 0 ? nil : pid
20
+ end
21
+
22
+ def self.fork_process(command, pid_filename, log_filename='/dev/null')
23
+ pid = fork do
24
+ STDERR.reopen(log_filename)
25
+ exec command
26
+ exit! 127 # safeguard in case exec fails
27
+ end
28
+ Process.detach(pid)
29
+
30
+ File.open pid_filename, 'w' do |f|
31
+ f.puts pid
32
+ end
33
+ end
34
+
35
+ def self.kill_process(pid_filename, options={})
36
+ if pid = self.read_pid(pid_filename)
37
+ begin
38
+ Process.kill 'TERM', pid
39
+ Process.wait pid if options[:wait]
40
+ rescue Errno::ESRCH
41
+ # no such process; assume it's already killed
42
+ end
43
+ end
44
+
45
+ begin
46
+ FileUtils.rm pid_filename
47
+ rescue Errno::ENOENT
48
+ # pid file already removed
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ require 'tempfile'
2
+
3
+ class Headless
4
+ class VideoRecorder
5
+ attr_accessor :pid_file_path, :tmp_file_path, :log_file_path
6
+
7
+ def initialize(display, dimensions, options = {})
8
+ CliUtil.ensure_application_exists!('ffmpeg', 'Ffmpeg not found on your system. Install it with sudo apt-get install ffmpeg')
9
+
10
+ @display = display
11
+ @dimensions = dimensions
12
+
13
+ @pid_file_path = options.fetch(:pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid")
14
+ @tmp_file_path = options.fetch(:tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov")
15
+ @log_file_path = options.fetch(:log_file_path, "/dev/null")
16
+ end
17
+
18
+ def capture_running?
19
+ !!@capture_running
20
+ end
21
+
22
+ 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
+ @capture_running = true
25
+ at_exit do
26
+ stop_and_discard if capture_running?
27
+ end
28
+ end
29
+
30
+ def stop_and_save(path)
31
+ @capture_running = false
32
+ CliUtil.kill_process(@pid_file_path, :wait => true)
33
+ FileUtils.mv(@tmp_file_path, path)
34
+ end
35
+
36
+ def stop_and_discard
37
+ @capture_running = false
38
+ CliUtil.kill_process(@pid_file_path, :wait => true)
39
+ begin
40
+ FileUtils.rm(@tmp_file_path)
41
+ rescue Errno::ENOENT
42
+ # that's ok if the file doesn't exist
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,125 @@
1
+ require 'lib/headless'
2
+
3
+ describe Headless do
4
+ before do
5
+ ENV['DISPLAY'] = ":31337"
6
+ stub_environment
7
+ end
8
+
9
+ context "instaniation" do
10
+ context "when Xvfb is not installed" do
11
+ before do
12
+ Headless::CliUtil.stub!(:application_exists?).and_return(false)
13
+ end
14
+
15
+ it "raises an error" do
16
+ lambda { Headless.new }.should raise_error(Headless::Exception)
17
+ end
18
+ end
19
+
20
+ context "when Xvfb not started yet" do
21
+ it "starts Xvfb" do
22
+ Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac >/dev/null 2>&1 &").and_return(true)
23
+
24
+ headless = Headless.new
25
+ end
26
+
27
+ it "allows setting screen dimensions" do
28
+ Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac >/dev/null 2>&1 &").and_return(true)
29
+
30
+ headless = Headless.new(:dimensions => "1024x768x16")
31
+ end
32
+ end
33
+
34
+ context "when Xvfb is already running" do
35
+ before do
36
+ Headless::CliUtil.stub!(:read_pid).and_return(31337)
37
+ end
38
+
39
+ it "raises an error if reuse display is not allowed" do
40
+ lambda { Headless.new(:reuse => false) }.should raise_error(Headless::Exception)
41
+ end
42
+
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)
46
+ end
47
+ end
48
+ end
49
+
50
+ context "lifecycle" do
51
+ let(:headless) { Headless.new }
52
+ describe "#start" do
53
+ it "switches to the headless server" do
54
+ ENV['DISPLAY'].should == ":31337"
55
+ headless.start
56
+ ENV['DISPLAY'].should == ":99"
57
+ end
58
+ end
59
+
60
+ describe "#stop" do
61
+ it "switches back from the headless server" do
62
+ ENV['DISPLAY'].should == ":31337"
63
+ headless.start
64
+ ENV['DISPLAY'].should == ":99"
65
+ headless.stop
66
+ ENV['DISPLAY'].should == ":31337"
67
+ end
68
+ end
69
+
70
+ describe "#destroy" do
71
+ before do
72
+ Headless::CliUtil.stub!(:read_pid).and_return(4444)
73
+ end
74
+
75
+ it "switches back from the headless server and terminates the headless session" do
76
+ Process.should_receive(:kill).with('TERM', 4444)
77
+
78
+ ENV['DISPLAY'].should == ":31337"
79
+ headless.start
80
+ ENV['DISPLAY'].should == ":99"
81
+ headless.destroy
82
+ ENV['DISPLAY'].should == ":31337"
83
+ end
84
+ end
85
+ end
86
+
87
+ context "#video" do
88
+ let(:headless) { Headless.new }
89
+
90
+ it "returns video recorder" do
91
+ headless.video.should be_a_kind_of(Headless::VideoRecorder)
92
+ end
93
+
94
+ it "returns the same instance" do
95
+ recorder = headless.video
96
+ headless.video.should be_eql(recorder)
97
+ end
98
+ end
99
+
100
+ context "#take_screenshot" do
101
+ let(:headless) { Headless.new }
102
+
103
+ it "raises an error if imagemagick is not installed" do
104
+ Headless::CliUtil.stub!(:application_exists?).and_return(false)
105
+
106
+ lambda { headless.take_screenshot }.should raise_error(Headless::Exception)
107
+ end
108
+
109
+ it "issues command to take screenshot" do
110
+ headless = Headless.new
111
+
112
+ Headless.any_instance.should_receive(:system)
113
+
114
+ headless.take_screenshot("/tmp/image.png")
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def stub_environment
121
+ Headless::CliUtil.stub!(:application_exists?).and_return(true)
122
+ Headless::CliUtil.stub!(:read_pid).and_return(nil)
123
+ Headless::CliUtil.stub!(:path_to).and_return("/usr/bin/Xvfb")
124
+ end
125
+ end
@@ -0,0 +1,60 @@
1
+ require 'lib/headless'
2
+
3
+ describe Headless::VideoRecorder do
4
+ before do
5
+ stub_environment
6
+ end
7
+
8
+ describe "instaniation" do
9
+ before do
10
+ Headless::CliUtil.stub!(:application_exists?).and_return(false)
11
+ end
12
+
13
+ it "throws an error if ffmpeg is not installed" do
14
+ lambda { Headless::VideoRecorder.new(99, "1024x768x32") }.should raise_error(Headless::Exception)
15
+ end
16
+ end
17
+
18
+ describe "#capture" do
19
+ it "starts ffmpeg" do
20
+ Headless::CliUtil.stub(:path_to, 'ffmpeg').and_return('ffmpeg')
21
+ Headless::CliUtil.should_receive(:fork_process).with(/ffmpeg -y -r 30 -g 600 -s 1024x768x32 -f x11grab -i :99 -vcodec qtrle/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
22
+
23
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32")
24
+ recorder.start_capture
25
+ end
26
+ end
27
+
28
+ context "stopping video recording" do
29
+ subject do
30
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => "/tmp/pid", :tmp_file_path => "/tmp/ci.mov")
31
+ recorder.start_capture
32
+ recorder
33
+ end
34
+
35
+ describe "using #stop_and_save" do
36
+ 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")
39
+
40
+ subject.stop_and_save("/tmp/test.mov")
41
+ end
42
+ end
43
+
44
+ describe "using #stop_and_discard" do
45
+ 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")
48
+
49
+ subject.stop_and_discard
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def stub_environment
57
+ Headless::CliUtil.stub!(:application_exists?).and_return(true)
58
+ Headless::CliUtil.stub!(:fork_process).and_return(true)
59
+ end
60
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: headless
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
5
- prerelease: false
4
+ hash: 21
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 2
8
9
  - 1
9
- - 0
10
- version: 0.1.0
10
+ version: 0.2.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Leonid Shevtsov
@@ -15,10 +15,24 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-16 00:00:00 +03:00
18
+ date: 2011-08-26 00:00:00 +03:00
19
19
  default_executable:
20
- dependencies: []
21
-
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 15
30
+ segments:
31
+ - 2
32
+ - 6
33
+ version: "2.6"
34
+ type: :development
35
+ version_requirements: *id001
22
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
37
  email: leonid@shevtsov.me
24
38
  executables: []
@@ -28,12 +42,20 @@ extensions: []
28
42
  extra_rdoc_files: []
29
43
 
30
44
  files:
31
- - lib/headless.rb
45
+ - .gitignore
32
46
  - CHANGELOG
33
- - README.md
47
+ - Gemfile
34
48
  - LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - headless.gemspec
52
+ - lib/headless.rb
53
+ - lib/headless/cli_util.rb
54
+ - lib/headless/video/video_recorder.rb
55
+ - spec/headless_spec.rb
56
+ - spec/video_recorder_spec.rb
35
57
  has_rdoc: true
36
- homepage: http://github.com/leonid-shevtsov/headless
58
+ homepage: http://leonid.shevtsov.me/en/headless
37
59
  licenses: []
38
60
 
39
61
  post_install_message:
@@ -62,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
84
  requirements:
63
85
  - Xvfb
64
86
  rubyforge_project:
65
- rubygems_version: 1.3.7
87
+ rubygems_version: 1.6.2
66
88
  signing_key:
67
89
  specification_version: 3
68
90
  summary: Ruby headless display interface