headless-muse 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +12 -0
- data/.travis/setup.sh +15 -0
- data/CHANGELOG +33 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +141 -0
- data/Rakefile +10 -0
- data/headless.gemspec +21 -0
- data/lib/headless.rb +183 -0
- data/lib/headless/cli_util.rb +64 -0
- data/lib/headless/video/video_recorder.rb +82 -0
- data/spec/cli_util_spec.rb +65 -0
- data/spec/headless_spec.rb +219 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/video_recorder_spec.rb +93 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f3ef46e8f92d17bf336aae8bab33f7e138dd4619
|
4
|
+
data.tar.gz: 36029054c983b680a1996121ae0256a52c5f034a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a4e524a612efbeab185f98efa58ba2c7490a391bc02bdee9a327b8760d62837ed76f575c4756c20aa3442978b17d0305bf0a261c8eb7e7391652c5e3b1006917
|
7
|
+
data.tar.gz: 0df9462f894b68182517fe89552b22fc5ce09f1143d44233efa46e9fd5dece0d559e049bb4311e752fb69757608871db0e0a922f1b4006cc087566d4b7b12254
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.travis/setup.sh
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
case "$FFMPEG_VERSION" in
|
3
|
+
2.3)
|
4
|
+
sudo add-apt-repository ppa:archivematica/externals -y
|
5
|
+
sudo apt-get update -q
|
6
|
+
sudo apt-get install ffmpeg
|
7
|
+
;;
|
8
|
+
|
9
|
+
1.2)
|
10
|
+
stop
|
11
|
+
;;
|
12
|
+
*)
|
13
|
+
sudo apt-get update -q
|
14
|
+
sudo apt-get install ffmpeg
|
15
|
+
esac
|
data/CHANGELOG
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
## 1.0.2 (2014-06-03)
|
2
|
+
|
3
|
+
* pass options correctly to ffmpeg (from @abotalov)
|
4
|
+
* only destroy headless if it was created (from @evandrodp)
|
5
|
+
|
6
|
+
## 1.0.1 (2013-02-20)
|
7
|
+
|
8
|
+
* when starting, wait for Xvfb to launch (fixed issue #33)
|
9
|
+
|
10
|
+
## 1.0.0 (2013-01-28)
|
11
|
+
|
12
|
+
* bugfix release
|
13
|
+
* version number compliant to the [semantic versioning system](http://semver.org)
|
14
|
+
|
15
|
+
## 0.3.1 (2012-03-29)
|
16
|
+
|
17
|
+
* added autopicking of display number, if the requested one is already taken
|
18
|
+
* fixed plenty of bugs thanks to @recursive, @gshakhn, @masatomo and @mabotelh
|
19
|
+
|
20
|
+
## 0.2.2 (2011-09-01)
|
21
|
+
|
22
|
+
* improve detection of ffmpeg process (from https://github.com/alanshields/headless)
|
23
|
+
|
24
|
+
## 0.2.1 (2011-08-26)
|
25
|
+
|
26
|
+
* added ability to capture screenshots (from https://github.com/iafonov/headless)
|
27
|
+
* added ability to capture video (from https://github.com/iafonov/headless)
|
28
|
+
* fixed issue with stray pidfile
|
29
|
+
|
30
|
+
## 0.1.0 (2010-08-15)
|
31
|
+
|
32
|
+
* introduced options
|
33
|
+
* make it possible to change virtual screen dimensions and pixel depth
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Leonid Shevtsov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# Headless
|
2
|
+
[![Build Status](https://travis-ci.org/pgeraghty/headless.svg?branch=master)](https://travis-ci.org/pgeraghty/headless)
|
3
|
+
[![Coverage Status](https://img.shields.io/coveralls/pgeraghty/headless.svg)](https://coveralls.io/r/pgeraghty/headless?branch=master)
|
4
|
+
|
5
|
+
# This fork
|
6
|
+
|
7
|
+
This fork is the source of the [headless-muse gem]().
|
8
|
+
|
9
|
+
## Notes by original author (Leonid Shevtsov)
|
10
|
+
|
11
|
+
Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
|
12
|
+
It can also capture images and video from the virtual framebuffer.
|
13
|
+
|
14
|
+
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.
|
15
|
+
Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
|
16
|
+
|
17
|
+
Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
|
18
|
+
|
19
|
+
[Changelog](https://github.com/leonid-shevtsov/headless/blob/master/CHANGELOG)
|
20
|
+
|
21
|
+
**Note: Headless will NOT hide most applications on OS X. [Here is a detailed explanation](https://github.com/leonid-shevtsov/headless/issues/31#issuecomment-8933108)**
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
On Debian/Ubuntu:
|
26
|
+
|
27
|
+
```sh
|
28
|
+
sudo apt-get install xvfb
|
29
|
+
gem install headless
|
30
|
+
```
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Block mode:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'rubygems'
|
38
|
+
require 'headless'
|
39
|
+
require 'selenium-webdriver'
|
40
|
+
|
41
|
+
Headless.ly do
|
42
|
+
driver = Selenium::WebDriver.for :firefox
|
43
|
+
driver.navigate.to 'http://google.com'
|
44
|
+
puts driver.title
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Object mode:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'rubygems'
|
52
|
+
require 'headless'
|
53
|
+
require 'selenium-webdriver'
|
54
|
+
|
55
|
+
headless = Headless.new
|
56
|
+
headless.start
|
57
|
+
|
58
|
+
driver = Selenium::WebDriver.for :firefox
|
59
|
+
driver.navigate.to 'http://google.com'
|
60
|
+
puts driver.title
|
61
|
+
|
62
|
+
headless.destroy
|
63
|
+
```
|
64
|
+
|
65
|
+
## Cucumber
|
66
|
+
|
67
|
+
Running cucumber headless is now as simple as adding a before and after hook in `features/support/env.rb`:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# change the condition to fit your setup
|
71
|
+
if Capybara.current_driver == :selenium
|
72
|
+
require 'headless'
|
73
|
+
|
74
|
+
headless = Headless.new
|
75
|
+
headless.start
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
## Running tests in parallel
|
80
|
+
|
81
|
+
If you have multiple threads running acceptance tests in parallel, you want to spawn Headless before forking, and then reuse that instance with `destroy_at_exit: false`.
|
82
|
+
You can even spawn a Headless instance in one ruby script, and then reuse the same instance in other scripts by specifying the same display number and `reuse: true`.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# spawn_headless.rb
|
86
|
+
Headless.new(display: 100, destroy_at_exit: false).start
|
87
|
+
|
88
|
+
# test_suite_that_could_be_ran_multiple_times.rb
|
89
|
+
Headless.new(display: 100, reuse: true, destroy_at_exit: false).start
|
90
|
+
|
91
|
+
# reap_headless.rb
|
92
|
+
headless = Headless.new(display: 100, reuse: true)
|
93
|
+
headless.destroy
|
94
|
+
```
|
95
|
+
|
96
|
+
|
97
|
+
## Cucumber with wkhtmltopdf
|
98
|
+
|
99
|
+
_Note: this is true for other programs which may use headless at the same time as cucumber is running_
|
100
|
+
|
101
|
+
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)`.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
headless = Headless.new(:display => '100')
|
105
|
+
headless.start
|
106
|
+
```
|
107
|
+
|
108
|
+
## Capturing video
|
109
|
+
|
110
|
+
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:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
require 'headless'
|
114
|
+
|
115
|
+
headless = Headless.new
|
116
|
+
headless.start
|
117
|
+
|
118
|
+
Before do
|
119
|
+
headless.video.start_capture
|
120
|
+
end
|
121
|
+
|
122
|
+
After do |scenario|
|
123
|
+
if scenario.failed?
|
124
|
+
headless.video.stop_and_save("/tmp/#{BUILD_ID}/#{scenario.name.split.join("_")}.mov")
|
125
|
+
else
|
126
|
+
headless.video.stop_and_discard
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
## Taking screenshots
|
132
|
+
|
133
|
+
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.
|
134
|
+
|
135
|
+
## Contributors
|
136
|
+
|
137
|
+
* [Igor Afonov](http://iafonov.github.com) - video and screenshot capturing functionality.
|
138
|
+
|
139
|
+
---
|
140
|
+
|
141
|
+
© 2011 Leonid Shevtsov, released under the MIT license
|
data/Rakefile
ADDED
data/headless.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.authors = ['Leonid Shevtsov', 'Igor Afonov', 'Paul Geraghty']
|
3
|
+
s.email = 'muse@appsthatcould.be'
|
4
|
+
|
5
|
+
s.name = 'headless-muse'
|
6
|
+
s.version = '1.1.0'
|
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
|
+
It can also capture video and audio via ffmpeg and take screenshots.
|
12
|
+
EOF
|
13
|
+
s.requirements = 'Xvfb'
|
14
|
+
s.homepage = 'https://github.com/pgeraghty/headless'
|
15
|
+
s.license = 'MIT'
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
|
18
|
+
s.add_development_dependency 'rake'
|
19
|
+
s.add_development_dependency 'rspec', '~> 2.6'
|
20
|
+
s.add_development_dependency('coveralls', '> 0') unless RUBY_VERSION == '1.8.7'
|
21
|
+
end
|
data/lib/headless.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'headless/cli_util'
|
2
|
+
require 'headless/video/video_recorder'
|
3
|
+
|
4
|
+
# A class incapsulating the creation and usage of a headless X server
|
5
|
+
#
|
6
|
+
# == Prerequisites
|
7
|
+
#
|
8
|
+
# * X Window System
|
9
|
+
# * Xvfb[http://en.wikipedia.org/wiki/Xvfb]
|
10
|
+
#
|
11
|
+
# == Usage
|
12
|
+
#
|
13
|
+
# Block mode:
|
14
|
+
#
|
15
|
+
# require 'rubygems'
|
16
|
+
# require 'headless'
|
17
|
+
# require 'selenium-webdriver'
|
18
|
+
#
|
19
|
+
# Headless.ly do
|
20
|
+
# driver = Selenium::WebDriver.for :firefox
|
21
|
+
# driver.navigate.to 'http://google.com'
|
22
|
+
# puts driver.title
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Object mode:
|
26
|
+
#
|
27
|
+
# require 'rubygems'
|
28
|
+
# require 'headless'
|
29
|
+
# require 'selenium-webdriver'
|
30
|
+
#
|
31
|
+
# headless = Headless.new
|
32
|
+
# headless.start
|
33
|
+
#
|
34
|
+
# driver = Selenium::WebDriver.for :firefox
|
35
|
+
# driver.navigate.to 'http://google.com'
|
36
|
+
# puts driver.title
|
37
|
+
#
|
38
|
+
# headless.destroy
|
39
|
+
#--
|
40
|
+
# TODO test that reuse actually works with an existing xvfb session
|
41
|
+
#++
|
42
|
+
class Headless
|
43
|
+
|
44
|
+
DEFAULT_DISPLAY_NUMBER = 99
|
45
|
+
MAX_DISPLAY_NUMBER = 10_000
|
46
|
+
DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
|
47
|
+
# How long should we wait for Xvfb to open a display, before assuming that it is frozen (in seconds)
|
48
|
+
XVFB_LAUNCH_TIMEOUT = 10
|
49
|
+
|
50
|
+
class Exception < RuntimeError
|
51
|
+
end
|
52
|
+
|
53
|
+
# The display number
|
54
|
+
attr_reader :display
|
55
|
+
|
56
|
+
# The display dimensions
|
57
|
+
attr_reader :dimensions
|
58
|
+
|
59
|
+
# Creates a new headless server, but does NOT switch to it immediately. Call #start for that
|
60
|
+
#
|
61
|
+
# List of available options:
|
62
|
+
# * +display+ (default 99) - what display number to listen to;
|
63
|
+
# * +reuse+ (default true) - if given display server already exists, should we use it or try another?
|
64
|
+
# * +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.
|
65
|
+
# * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
|
66
|
+
# * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
|
67
|
+
def initialize(options = {})
|
68
|
+
CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
|
69
|
+
|
70
|
+
@display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
|
71
|
+
@autopick_display = options.fetch(:autopick, !options.key?(:display))
|
72
|
+
@reuse_display = options.fetch(:reuse, true)
|
73
|
+
@dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
|
74
|
+
@video_capture_options = options.fetch(:video, {})
|
75
|
+
@destroy_at_exit = options.fetch(:destroy_at_exit, true)
|
76
|
+
|
77
|
+
# FIXME Xvfb launch should not happen inside the constructor
|
78
|
+
attach_xvfb
|
79
|
+
end
|
80
|
+
|
81
|
+
# Switches to the headless server
|
82
|
+
def start
|
83
|
+
@old_display = ENV['DISPLAY']
|
84
|
+
ENV['DISPLAY'] = ":#{display}"
|
85
|
+
hook_at_exit
|
86
|
+
end
|
87
|
+
|
88
|
+
# Switches back from the headless server
|
89
|
+
def stop
|
90
|
+
ENV['DISPLAY'] = @old_display
|
91
|
+
end
|
92
|
+
|
93
|
+
# Switches back from the headless server and terminates the headless session
|
94
|
+
def destroy
|
95
|
+
stop
|
96
|
+
CliUtil.kill_process(pid_filename)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Block syntax:
|
100
|
+
#
|
101
|
+
# Headless.run do
|
102
|
+
# # perform operations in headless mode
|
103
|
+
# end
|
104
|
+
# See #new for options
|
105
|
+
def self.run(options={}, &block)
|
106
|
+
headless = Headless.new(options)
|
107
|
+
headless.start
|
108
|
+
yield headless
|
109
|
+
ensure
|
110
|
+
headless && headless.destroy
|
111
|
+
end
|
112
|
+
class <<self; alias_method :ly, :run; end
|
113
|
+
|
114
|
+
def video
|
115
|
+
@video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
|
116
|
+
end
|
117
|
+
|
118
|
+
def take_screenshot(file_path)
|
119
|
+
CliUtil.ensure_application_exists!('import', "imagemagick not found on your system. Please install it using sudo apt-get install imagemagick")
|
120
|
+
|
121
|
+
system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}"
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def attach_xvfb
|
127
|
+
possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display)
|
128
|
+
pick_available_display(possible_display_set, @reuse_display)
|
129
|
+
end
|
130
|
+
|
131
|
+
def pick_available_display(display_set, can_reuse)
|
132
|
+
display_set.each do |display_number|
|
133
|
+
@display = display_number
|
134
|
+
begin
|
135
|
+
return true if xvfb_running? && can_reuse
|
136
|
+
return true if !xvfb_running? && launch_xvfb
|
137
|
+
rescue Errno::EPERM # display not accessible
|
138
|
+
next
|
139
|
+
end
|
140
|
+
end
|
141
|
+
raise Headless::Exception.new("Could not find an available display")
|
142
|
+
end
|
143
|
+
|
144
|
+
def launch_xvfb
|
145
|
+
#TODO error reporting
|
146
|
+
result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
|
147
|
+
raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless result
|
148
|
+
ensure_xvfb_is_running
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
|
152
|
+
def ensure_xvfb_is_running
|
153
|
+
start_time = Time.now
|
154
|
+
begin
|
155
|
+
sleep 0.01 # to avoid cpu hogging
|
156
|
+
raise Headless::Exception.new("Xvfb is frozen") if (Time.now-start_time)>=XVFB_LAUNCH_TIMEOUT
|
157
|
+
end while !xvfb_running?
|
158
|
+
end
|
159
|
+
|
160
|
+
def xvfb_running?
|
161
|
+
!!read_xvfb_pid
|
162
|
+
end
|
163
|
+
|
164
|
+
def pid_filename
|
165
|
+
"/tmp/.X#{display}-lock"
|
166
|
+
end
|
167
|
+
|
168
|
+
def read_xvfb_pid
|
169
|
+
CliUtil.read_pid(pid_filename)
|
170
|
+
end
|
171
|
+
|
172
|
+
def hook_at_exit
|
173
|
+
unless @at_exit_hook_installed
|
174
|
+
@at_exit_hook_installed = true
|
175
|
+
at_exit do
|
176
|
+
exit_status = $!.status if $!.is_a?(SystemExit)
|
177
|
+
destroy if @destroy_at_exit
|
178
|
+
exit exit_status if exit_status
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
@@ -0,0 +1,64 @@
|
|
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 = nil if pid.zero?
|
20
|
+
|
21
|
+
if pid
|
22
|
+
begin
|
23
|
+
Process.kill(0, pid)
|
24
|
+
pid
|
25
|
+
rescue Errno::ESRCH
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.fork_process(command, pid_filename, log_filename='/dev/null')
|
34
|
+
pid = fork do
|
35
|
+
STDERR.reopen(log_filename)
|
36
|
+
exec command
|
37
|
+
exit! 127 # safeguard in case exec fails
|
38
|
+
end
|
39
|
+
|
40
|
+
File.open pid_filename, 'w' do |f|
|
41
|
+
f.puts pid
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.kill_process(pid_filename, options={})
|
46
|
+
if pid = self.read_pid(pid_filename)
|
47
|
+
begin
|
48
|
+
Process.kill 'TERM', pid
|
49
|
+
Process.wait pid if options[:wait]
|
50
|
+
rescue Errno::ESRCH
|
51
|
+
# no such process; assume it's already killed
|
52
|
+
rescue Errno::ECHILD
|
53
|
+
# Process.wait tried to wait on a dead process
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
FileUtils.rm pid_filename
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
# pid file already removed
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
class Headless
|
4
|
+
class VideoRecorder
|
5
|
+
attr_accessor :pid_file_path, :tmp_file_path, :log_file_path, :bin_file_path, :bin_version, :capture_with
|
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[/.+(?=x)/]
|
12
|
+
|
13
|
+
@bin_file_path = options.fetch(:bin_file_path, CliUtil.path_to('ffmpeg'))
|
14
|
+
# divine version - tested on:
|
15
|
+
# ffmpeg version 2.3.1
|
16
|
+
# ffmpeg version 0.10.9-7:0.10.9-1~quantal1
|
17
|
+
# ffmpeg 0.8.10-6:0.8.10-0ubuntu0.12.10.1
|
18
|
+
@bin_version = options.fetch(:bin_version, guess_ffmpeg_version!)
|
19
|
+
@pid_file_path = options.fetch(:pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid")
|
20
|
+
@tmp_file_path = options.fetch(:tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov")
|
21
|
+
@log_file_path = options.fetch(:log_file_path, '/dev/null')
|
22
|
+
@codec = options.fetch(:codec, 'qtrle')
|
23
|
+
@frame_rate = options.fetch(:frame_rate, 30).to_i
|
24
|
+
@nomouse = options.fetch(:nomouse, false)
|
25
|
+
@audio = options.fetch(:audio, false)
|
26
|
+
end
|
27
|
+
|
28
|
+
def guess_ffmpeg_version!
|
29
|
+
(Gem::Version.new(`#{@bin_file_path} -version`[/(?:ffmpeg )(?:version )?((?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+))/, 1])) unless @bin_file_path.empty? rescue Gem::Version.new('0')
|
30
|
+
end
|
31
|
+
|
32
|
+
def capture_running?
|
33
|
+
!!CliUtil.read_pid(@pid_file_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def capture_with
|
37
|
+
# TODO adjust switches based on @bin_version e.g. if @bin_version < Gem::Version.new('1')
|
38
|
+
[
|
39
|
+
@bin_file_path,
|
40
|
+
'y', # ignore already-existing file
|
41
|
+
('f alsa -ac 2 -i hw:0,1' if @audio),
|
42
|
+
"r #{@frame_rate}",
|
43
|
+
"s #{@dimensions}",
|
44
|
+
'f x11grab',
|
45
|
+
('draw_mouse 0' if @nomouse),
|
46
|
+
"i :#{@display}",
|
47
|
+
"vcodec #{@codec}",
|
48
|
+
("g #{@frame_rate.to_i*20}" if @bin_version && @bin_version < Gem::Version.new('1'))
|
49
|
+
].compact*' -'
|
50
|
+
end
|
51
|
+
|
52
|
+
def start_capture
|
53
|
+
CliUtil.fork_process("#{capture_with} #{@tmp_file_path}", @pid_file_path, @log_file_path)
|
54
|
+
at_exit do
|
55
|
+
exit_status = $!.status if $!.is_a?(SystemExit)
|
56
|
+
stop_and_discard
|
57
|
+
exit exit_status if exit_status
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def stop_and_save(path)
|
62
|
+
CliUtil.kill_process(@pid_file_path, :wait => true)
|
63
|
+
if File.exists? @tmp_file_path
|
64
|
+
begin
|
65
|
+
FileUtils.mv(@tmp_file_path, path)
|
66
|
+
true
|
67
|
+
rescue Errno::EINVAL
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop_and_discard
|
74
|
+
CliUtil.kill_process(@pid_file_path, :wait => true)
|
75
|
+
begin
|
76
|
+
FileUtils.rm(@tmp_file_path)
|
77
|
+
rescue Errno::ENOENT
|
78
|
+
# that's ok if the file doesn't exist
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Headless::CliUtil do
|
4
|
+
before do
|
5
|
+
subject.class.stub(:path_to).and_return('ffmpeg')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'application_exists?' do
|
9
|
+
before { subject.class.stub(:`).and_return('/usr/bin/ffmpeg') }
|
10
|
+
|
11
|
+
it 'calls which to find the process' do
|
12
|
+
subject.class.should_receive(:`).with('which ffmpeg').and_return('/usr/bin/ffmpeg')
|
13
|
+
subject.class.application_exists?('ffmpeg').should eq(true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'fork_process' do
|
18
|
+
before { subject.class.stub(:application_exists?).and_return(true) }
|
19
|
+
|
20
|
+
it 'forks' do
|
21
|
+
recorder = Headless::VideoRecorder.new(99, '1024x768x32')
|
22
|
+
|
23
|
+
subject.class.should_receive(:fork).and_yield do |block|
|
24
|
+
block.stub(:exec).and_return(123)
|
25
|
+
block.stub(:exit!)
|
26
|
+
STDERR.should_receive(:reopen).with('/dev/null')
|
27
|
+
block.should_receive(:exec).with("#{recorder.capture_with} #{recorder.tmp_file_path}")
|
28
|
+
end
|
29
|
+
recorder.start_capture
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates PID file' do
|
33
|
+
recorder = Headless::VideoRecorder.new(99, '1024x768x32')
|
34
|
+
subject.class.stub(:fork).and_return(123)
|
35
|
+
|
36
|
+
file = double('file')
|
37
|
+
File.should_receive(:open).with('/tmp/.headless_ffmpeg_99.pid', 'w').and_yield(file)
|
38
|
+
file.should_receive(:puts).with(123)
|
39
|
+
|
40
|
+
recorder.start_capture
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'read_pid' do
|
45
|
+
before do
|
46
|
+
Process.stub(:kill)
|
47
|
+
File.stub(:read).and_return('999999999999')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'reads PID file' do
|
51
|
+
File.should_receive(:read).with('/tmp/.headless_ffmpeg_99.pid').and_return('')
|
52
|
+
subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'sends signal to process' do
|
56
|
+
Process.should_receive(:kill).with(0, 999999999999)
|
57
|
+
subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(999999999999)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns nil after a rescued error when process does not exist' do
|
61
|
+
Process.stub(:kill).and_raise(Errno::ESRCH)
|
62
|
+
subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(nil)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Headless do
|
4
|
+
before do
|
5
|
+
ENV['DISPLAY'] = ":31337"
|
6
|
+
stub_environment
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
`killall Xvfb`
|
11
|
+
# RSpec::Mocks.proxy_for(Headless::CliUtil).reset
|
12
|
+
# RSpec::Mocks.proxy_for(Headless).reset
|
13
|
+
#
|
14
|
+
# ObjectSpace.each_object(Headless) { |h| RSpec::Mocks.proxy_for(h).reset; h.destroy }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "instantiation" do
|
18
|
+
context "when Xvfb is not installed" do
|
19
|
+
before do
|
20
|
+
Headless::CliUtil.stub(:application_exists?).and_return(false)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "raises an error" do
|
24
|
+
lambda { Headless.new }.should raise_error(Headless::Exception)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "when Xvfb is not started yet" do
|
29
|
+
it "starts Xvfb" do
|
30
|
+
Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac >/dev/null 2>&1 &").and_return(true)
|
31
|
+
|
32
|
+
headless = Headless.new
|
33
|
+
end
|
34
|
+
|
35
|
+
it "allows setting screen dimensions" do
|
36
|
+
Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac >/dev/null 2>&1 &").and_return(true)
|
37
|
+
|
38
|
+
headless = Headless.new(:dimensions => "1024x768x16")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when Xvfb is already running" do
|
43
|
+
before do
|
44
|
+
Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock').and_return(31337)
|
45
|
+
Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
context "and display reuse is allowed" do
|
49
|
+
let(:options) { {:reuse => true} }
|
50
|
+
|
51
|
+
it "should reuse the existing Xvfb" do
|
52
|
+
Headless.new(options).display.should == 99
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "and display reuse is not allowed" do
|
57
|
+
let(:options) { {:reuse => false} }
|
58
|
+
|
59
|
+
it "should pick the next available display number" do
|
60
|
+
Headless.new(options).display.should == 100
|
61
|
+
end
|
62
|
+
|
63
|
+
context "and display number is explicitly set" do
|
64
|
+
let(:options) { {:reuse => false, :display => 99} }
|
65
|
+
|
66
|
+
it "should fail with an exception" do
|
67
|
+
lambda { Headless.new(options) }.should raise_error(Headless::Exception)
|
68
|
+
end
|
69
|
+
|
70
|
+
context "and autopicking is allowed" do
|
71
|
+
let(:options) { {:reuse => false, :display => 99, :autopick => true} }
|
72
|
+
|
73
|
+
it "should pick the next available display number" do
|
74
|
+
Headless.new(options).display.should == 100
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when Xvfb is started, but by another user' do
|
82
|
+
before do
|
83
|
+
Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
|
84
|
+
Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
context "and display autopicking is not allowed" do
|
88
|
+
let(:options) { {:autopick => false} }
|
89
|
+
|
90
|
+
it "should fail with and exception" do
|
91
|
+
lambda { Headless.new(options) }.should raise_error(Headless::Exception)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "and display autopicking is allowed" do
|
96
|
+
let(:options) { {:autopick => true} }
|
97
|
+
|
98
|
+
it "should pick the next display number" do
|
99
|
+
Headless.new(options).display.should == 100
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "lifecycle" do
|
106
|
+
let(:headless) { Headless.new }
|
107
|
+
describe "#start" do
|
108
|
+
it "switches to the headless server" do
|
109
|
+
ENV['DISPLAY'].should == ":31337"
|
110
|
+
headless.start
|
111
|
+
ENV['DISPLAY'].should == ":99"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#stop" do
|
116
|
+
it "switches back from the headless server" do
|
117
|
+
ENV['DISPLAY'].should == ":31337"
|
118
|
+
headless.start
|
119
|
+
ENV['DISPLAY'].should == ":99"
|
120
|
+
headless.stop
|
121
|
+
ENV['DISPLAY'].should == ":31337"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "#destroy" do
|
126
|
+
before do
|
127
|
+
Headless::CliUtil.stub(:read_pid).and_return(4444)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "switches back from the headless server and terminates the headless session" do
|
131
|
+
Process.should_receive(:kill).with('TERM', 4444)
|
132
|
+
|
133
|
+
ENV['DISPLAY'].should == ":31337"
|
134
|
+
headless.start
|
135
|
+
ENV['DISPLAY'].should == ":99"
|
136
|
+
headless.destroy
|
137
|
+
ENV['DISPLAY'].should == ":31337"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "#video" do
|
143
|
+
let(:headless) { Headless.new }
|
144
|
+
|
145
|
+
it "returns video recorder" do
|
146
|
+
headless.video.should be_a_kind_of(Headless::VideoRecorder)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "returns the same instance" do
|
150
|
+
recorder = headless.video
|
151
|
+
headless.video.should be_eql(recorder)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context "#take_screenshot" do
|
156
|
+
let(:headless) { Headless.new }
|
157
|
+
|
158
|
+
it "raises an error if imagemagick is not installed" do
|
159
|
+
Headless::CliUtil.stub(:application_exists?).and_return(false)
|
160
|
+
|
161
|
+
expect { headless.take_screenshot }.to raise_error
|
162
|
+
end
|
163
|
+
|
164
|
+
it "issues command to take screenshot" do
|
165
|
+
headless = Headless.new
|
166
|
+
|
167
|
+
Headless.any_instance.should_receive(:system)
|
168
|
+
|
169
|
+
headless.take_screenshot("/tmp/image.png")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context '#ensure_xvfb_is_running' do
|
174
|
+
let(:headless) { Headless.new }
|
175
|
+
before { headless.stub(:ensure_xvfb_is_running).and_call_original }
|
176
|
+
|
177
|
+
|
178
|
+
it 'store the start times and compares' do
|
179
|
+
headless.stub(:xvfb_running?).and_return(true)
|
180
|
+
Time.should_receive(:now).twice.and_call_original
|
181
|
+
headless.ensure_xvfb_is_running
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'pauses briefly' do
|
185
|
+
headless.stub(:xvfb_running?).and_return(true)
|
186
|
+
headless.should_receive(:sleep).with(0.01)
|
187
|
+
headless.ensure_xvfb_is_running
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'times out' do
|
191
|
+
headless.stub(:xvfb_running?).and_return(false)
|
192
|
+
times = [Time.now + 20, Time.now]
|
193
|
+
Time.stub(:now) do
|
194
|
+
times.pop
|
195
|
+
end
|
196
|
+
expect { headless.ensure_xvfb_is_running }.to raise_error(Headless::Exception)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'run' do
|
201
|
+
it 'instantiates, starts, yields and is destroyed' do
|
202
|
+
Headless.any_instance.should_receive(:start)
|
203
|
+
Headless.any_instance.should_receive(:destroy)
|
204
|
+
expect { |b| Headless.run({}, &b) }.to yield_with_args(Headless)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
def stub_environment
|
210
|
+
Headless::CliUtil.stub(:application_exists?).and_return(true)
|
211
|
+
Headless::CliUtil.stub(:read_pid).and_return(nil)
|
212
|
+
Headless::CliUtil.stub(:path_to)
|
213
|
+
Headless::CliUtil.stub(:path_to).with('Xvfb').and_return('/usr/bin/Xvfb')
|
214
|
+
Headless::CliUtil.stub(:path_to).with('ffmpeg').and_return('/usr/bin/ffmpeg')
|
215
|
+
|
216
|
+
# TODO this is wrong. But, as long as Xvfb is started inside the constructor (which is also wrong), I don't see another option to make tests pass
|
217
|
+
Headless.any_instance.stub(:ensure_xvfb_is_running).and_return(true)
|
218
|
+
end
|
219
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Headless::VideoRecorder do
|
4
|
+
before do
|
5
|
+
stub_environment
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "instantiation" 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).and_return('ffmpeg')
|
21
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32")
|
22
|
+
|
23
|
+
Headless::CliUtil.should_receive(:fork_process).with(/#{recorder.capture_with}/, '/tmp/.headless_ffmpeg_99.pid', '/dev/null')
|
24
|
+
recorder.start_capture
|
25
|
+
end
|
26
|
+
|
27
|
+
it "starts ffmpeg with specified codec" do
|
28
|
+
Headless::CliUtil.stub(:path_to).and_return('ffmpeg')
|
29
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'})
|
30
|
+
Headless::CliUtil.should_receive(:fork_process).with(/#{recorder.capture_with}/, '/tmp/.headless_ffmpeg_99.pid', '/dev/null')
|
31
|
+
recorder.start_capture
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "stopping video recording" do
|
36
|
+
let(:tmpfile) { '/tmp/ci.mov' }
|
37
|
+
let(:filename) { '/tmp/test.mov' }
|
38
|
+
let(:pidfile) { '/tmp/pid' }
|
39
|
+
|
40
|
+
subject do
|
41
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile)
|
42
|
+
recorder.start_capture
|
43
|
+
recorder
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "using #stop_and_save" do
|
47
|
+
it "stops video recording and saves file" do
|
48
|
+
Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
|
49
|
+
File.should_receive(:exists?).with(tmpfile).and_return(true)
|
50
|
+
FileUtils.should_receive(:mv).with(tmpfile, filename)
|
51
|
+
|
52
|
+
expect(subject.stop_and_save(filename)).to eq(true)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns false after a rescued error when attempting to move file' do
|
56
|
+
FileUtils.stub(:mv).and_raise(Errno::EINVAL)
|
57
|
+
File.should_receive(:exists?).and_return(true)
|
58
|
+
|
59
|
+
expect(subject.stop_and_save(tmpfile)).to eq(false)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns nil when target file does not exist' do
|
63
|
+
File.stub(:exists?).and_return(false)
|
64
|
+
|
65
|
+
expect(subject.stop_and_save(tmpfile)).to eq(nil)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "using #stop_and_discard" do
|
70
|
+
it "stops video recording and deletes temporary file" do
|
71
|
+
Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
|
72
|
+
FileUtils.should_receive(:rm).with(tmpfile)
|
73
|
+
|
74
|
+
subject.stop_and_discard
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#capture_running?' do
|
79
|
+
it 'returns false unless the PID file exists' do
|
80
|
+
Headless::CliUtil.should_receive(:read_pid).with(pidfile)
|
81
|
+
|
82
|
+
expect(subject.capture_running?).to eq(false)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def stub_environment
|
90
|
+
Headless::CliUtil.stub(:application_exists?).and_return(true)
|
91
|
+
Headless::CliUtil.stub(:fork_process).and_return(true)
|
92
|
+
end
|
93
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: headless-muse
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Leonid Shevtsov
|
8
|
+
- Igor Afonov
|
9
|
+
- Paul Geraghty
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-08-17 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rspec
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2.6'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.6'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: coveralls
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
description: |2
|
58
|
+
Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
|
59
|
+
It can also capture video and audio via ffmpeg and take screenshots.
|
60
|
+
email: muse@appsthatcould.be
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- ".gitignore"
|
66
|
+
- ".travis.yml"
|
67
|
+
- ".travis/setup.sh"
|
68
|
+
- CHANGELOG
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- headless.gemspec
|
74
|
+
- lib/headless.rb
|
75
|
+
- lib/headless/cli_util.rb
|
76
|
+
- lib/headless/video/video_recorder.rb
|
77
|
+
- spec/cli_util_spec.rb
|
78
|
+
- spec/headless_spec.rb
|
79
|
+
- spec/spec_helper.rb
|
80
|
+
- spec/video_recorder_spec.rb
|
81
|
+
homepage: https://github.com/pgeraghty/headless
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements:
|
100
|
+
- Xvfb
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.2.2
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Ruby headless display interface
|
106
|
+
test_files: []
|