process_watcher 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +20 -0
- data/README.rdoc +66 -0
- data/Rakefile +69 -0
- data/lib/process_watcher.rb +155 -0
- data/lib/process_watcher/linux/process_monitor.rb +102 -0
- data/lib/process_watcher/version.rb +3 -0
- data/lib/process_watcher/watcher.rb +156 -0
- data/lib/process_watcher/win32/process_monitor.rb +96 -0
- data/process_watcher.gemspec +29 -0
- data/spec/process_watcher/watcher_spec.rb +80 -0
- data/spec/process_watcher_spec.rb +112 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +29 -0
- metadata +107 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
process_watcher (0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
rspec (1.3.0)
|
10
|
+
win32-api (1.4.6)
|
11
|
+
win32-process (0.6.2)
|
12
|
+
windows-pr (>= 1.0.5)
|
13
|
+
windows-api (0.4.0)
|
14
|
+
win32-api (>= 1.4.5)
|
15
|
+
windows-pr (1.0.9)
|
16
|
+
win32-api (>= 1.4.5)
|
17
|
+
windows-api (>= 0.3.0)
|
18
|
+
|
19
|
+
PLATFORMS
|
20
|
+
ruby
|
21
|
+
|
22
|
+
DEPENDENCIES
|
23
|
+
process_watcher!
|
24
|
+
rspec (~> 1.3)
|
25
|
+
win32-process (~> 0.6.1)
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 RightScale, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
= ProcessWatcher
|
2
|
+
|
3
|
+
== DESCRIPTION
|
4
|
+
|
5
|
+
=== Synopsis
|
6
|
+
|
7
|
+
ProcessWatcher is a cross platform interface for running subprocesses
|
8
|
+
safely. Unlike backticks or popen in Ruby 1.8, it will not invoke a
|
9
|
+
shell. Unlike system, it will permits capturing the output. Unlike
|
10
|
+
all native Ruby interfaces, it permits killing the subprocess if it
|
11
|
+
consume too much space or takes too long. And it is easier to use on
|
12
|
+
Windows than rolling it by hand.
|
13
|
+
|
14
|
+
== USAGE
|
15
|
+
|
16
|
+
=== Simple Example
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
require 'process_watcher'
|
20
|
+
|
21
|
+
# Will raise ProcessWatcher::NonzeroExitCode if the exit code is not 0
|
22
|
+
output = ProcessWatcher.watch(command, [arguments])
|
23
|
+
|
24
|
+
# Will just run to completion and return the exit status
|
25
|
+
output, status = ProcessWatcher.run(command, arg1, arg2, ...)
|
26
|
+
|
27
|
+
== INSTALLATION
|
28
|
+
|
29
|
+
This gem can be installed by entering the following at the command
|
30
|
+
prompt:
|
31
|
+
|
32
|
+
gem install process_watcher
|
33
|
+
|
34
|
+
== TESTING
|
35
|
+
|
36
|
+
Install the following RubyGems required for testing:
|
37
|
+
- rspec
|
38
|
+
|
39
|
+
The build can be tested using the RSpec gem.
|
40
|
+
|
41
|
+
rake spec
|
42
|
+
|
43
|
+
== LICENSE
|
44
|
+
|
45
|
+
<b>ProcessWatcher</b>
|
46
|
+
|
47
|
+
Copyright:: Copyright (c) 2010 RightScale, Inc.
|
48
|
+
|
49
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
50
|
+
a copy of this software and associated documentation files (the
|
51
|
+
'Software'), to deal in the Software without restriction, including
|
52
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
53
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
54
|
+
permit persons to whom the Software is furnished to do so, subject to
|
55
|
+
the following conditions:
|
56
|
+
|
57
|
+
The above copyright notice and this permission notice shall be
|
58
|
+
included in all copies or substantial portions of the Software.
|
59
|
+
|
60
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
61
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
62
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
63
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
64
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
65
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
66
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'rake'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/clean'
|
8
|
+
|
9
|
+
Bundler::GemHelper.install_tasks
|
10
|
+
|
11
|
+
# == Gem == #
|
12
|
+
|
13
|
+
gemtask = Rake::GemPackageTask.new(Gem::Specification.load("process_watcher.gemspec")) do |package|
|
14
|
+
package.package_dir = ENV['PACKAGE_DIR'] || 'pkg'
|
15
|
+
package.need_zip = true
|
16
|
+
package.need_tar = true
|
17
|
+
end
|
18
|
+
|
19
|
+
directory gemtask.package_dir
|
20
|
+
|
21
|
+
CLEAN.include(gemtask.package_dir)
|
22
|
+
|
23
|
+
# == Unit Tests == #
|
24
|
+
|
25
|
+
task :specs => :spec
|
26
|
+
|
27
|
+
desc "Run unit tests"
|
28
|
+
Spec::Rake::SpecTask.new do |t|
|
29
|
+
t.spec_files = Dir['spec/**/*_spec.rb']
|
30
|
+
t.spec_opts = lambda do
|
31
|
+
IO.readlines(File.join(File.dirname(__FILE__), 'spec', 'spec.opts')).map {|l| l.chomp.split " "}.flatten
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Run unit tests with RCov"
|
36
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
37
|
+
t.spec_files = Dir['spec/**/*_spec.rb']
|
38
|
+
t.rcov = true
|
39
|
+
t.rcov_opts = lambda do
|
40
|
+
IO.readlines(File.join(File.dirname(__FILE__), 'spec', 'rcov.opts')).map {|l| l.chomp.split " "}.flatten
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Print Specdoc for unit tests"
|
45
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
46
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
47
|
+
t.spec_files = Dir['spec/**/*_spec.rb']
|
48
|
+
end
|
49
|
+
|
50
|
+
# == Documentation == #
|
51
|
+
|
52
|
+
desc "Generate API documentation to doc/rdocs/index.html"
|
53
|
+
Rake::RDocTask.new do |rd|
|
54
|
+
rd.rdoc_dir = 'doc/rdocs'
|
55
|
+
rd.main = 'README.rdoc'
|
56
|
+
rd.rdoc_files.include 'README.rdoc', "lib/**/*.rb"
|
57
|
+
|
58
|
+
rd.options << '--inline-source'
|
59
|
+
rd.options << '--line-numbers'
|
60
|
+
rd.options << '--all'
|
61
|
+
rd.options << '--fileboxes'
|
62
|
+
rd.options << '--diagram'
|
63
|
+
end
|
64
|
+
|
65
|
+
# == Emacs integration == #
|
66
|
+
desc "Rebuild TAGS file"
|
67
|
+
task :tags do
|
68
|
+
sh "rtags -R lib spec"
|
69
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'shellwords'
|
25
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'process_watcher', 'watcher'))
|
26
|
+
|
27
|
+
# Convenient interface to process watching functionality.
|
28
|
+
module ProcessWatcher
|
29
|
+
# Raised when a subprocess took too much time to run.
|
30
|
+
class TimeoutError < RuntimeError
|
31
|
+
# Describe the error.
|
32
|
+
def to_s
|
33
|
+
"Command took too much time"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# Raised when a subprocess consumed too much space while running.
|
37
|
+
class TooMuchSpaceError < RuntimeError
|
38
|
+
# Describe the error.
|
39
|
+
def to_s
|
40
|
+
"Command took too much space"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
# Raised when a subprocess completed, but with a nonzero exit code.
|
44
|
+
class NonzeroExitCode < RuntimeError
|
45
|
+
# (Fixnum) exit code returned by the subprocess
|
46
|
+
attr_reader :exit_code
|
47
|
+
# (String) process output (hopefully explaining the situation)
|
48
|
+
attr_reader :output
|
49
|
+
|
50
|
+
def initialize(exit_code, output)
|
51
|
+
@exit_code = exit_code
|
52
|
+
@output = output
|
53
|
+
end
|
54
|
+
|
55
|
+
# Describe the error.
|
56
|
+
def to_s
|
57
|
+
"Exit code nonzero: #{@exit_code}\nOutput was #{@output}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
# Watch command, respecting +max_bytes+ and +max_seconds+. Returns
|
61
|
+
# the output of the command, with STDOUT and STDERR interleaved, if
|
62
|
+
# the command completed successfully. Will raise one of three
|
63
|
+
# errors in exceptional cases:
|
64
|
+
# TimeoutError:: if the process takes longer than max_seconds to run
|
65
|
+
# TooMuchSpaceError:: if the process consumes too much space in +dir+
|
66
|
+
# NonzeroExitCode:: if the process exits, but with a nonzero exit code
|
67
|
+
#
|
68
|
+
# This method accepts a block. If that block exists, it is called
|
69
|
+
# at most twice with three arguments, +phase+, +command+ and
|
70
|
+
# +exception+. The block is called once with +phase+ set to :begin,
|
71
|
+
# +command+ describing the command run, and +exception+ being nil.
|
72
|
+
# If execution completes normally, the block is called with +phase+
|
73
|
+
# set to :commit, +command+ again describing the command, and
|
74
|
+
# +exception+ nil. If an abnormal termination occurs (for example,
|
75
|
+
# running out of space), the block is called with +phase+ set to
|
76
|
+
# :abort, +command+ again describing the command and +exception+ set
|
77
|
+
# to the exception in question.
|
78
|
+
#
|
79
|
+
# === Parameters
|
80
|
+
# command(String):: command to run
|
81
|
+
# args(Array):: arguments for the command
|
82
|
+
# dir(String):: directory to monitor (defaults to '.')
|
83
|
+
# max_bytes(Integer):: maximum number of bytes to permit
|
84
|
+
# (defaults to no restriction)
|
85
|
+
# max_seconds(Integer):: maximum number of seconds to permit to run
|
86
|
+
# (defaults to no restriction)
|
87
|
+
#
|
88
|
+
# === Block parameters
|
89
|
+
# phase(Keyword):: one of :begin, :commit, or :abort
|
90
|
+
# command(String):: description of what command is being run and
|
91
|
+
# where it is being run
|
92
|
+
# exception(Exception):: if non nil, the exception raised during
|
93
|
+
# the running of the subprocess
|
94
|
+
#
|
95
|
+
# === Returns
|
96
|
+
# String:: output of command
|
97
|
+
def self.watch(command, args, dir='.', max_bytes=-1, max_seconds=-1) # :yields: phase, command, exception
|
98
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes, max_seconds)
|
99
|
+
text = "in #{dir}, running #{command} #{Shellwords.join(args)}"
|
100
|
+
block_given? and yield :begin, text, nil
|
101
|
+
begin
|
102
|
+
result = watcher.launch_and_watch(command, args, dir)
|
103
|
+
if result.status == :timeout
|
104
|
+
raise TimeoutError, "command took too much time"
|
105
|
+
elsif result.status == :size_exceeded
|
106
|
+
raise TooMuchSpaceError, "command took too much space"
|
107
|
+
elsif result.exit_code != 0
|
108
|
+
raise NonzeroExitCode.new(result.exit_code, result.output), "nonzero exit code"
|
109
|
+
else
|
110
|
+
result.output
|
111
|
+
end
|
112
|
+
rescue => e
|
113
|
+
block_given? and yield :abort, text, e
|
114
|
+
raise
|
115
|
+
else
|
116
|
+
block_given? and yield :commit, text, nil
|
117
|
+
result.output
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Spawn given process, wait for it to complete, and return its
|
122
|
+
# output and the exit status of the process. Functions similarly to
|
123
|
+
# the backtick operator, only it avoids invoking the command
|
124
|
+
# interpreter under operating systems that support fork-and-exec.
|
125
|
+
#
|
126
|
+
# This method accepts a variable number of parameters; the first
|
127
|
+
# param is always the command to run; successive parameters are
|
128
|
+
# command-line arguments for the process.
|
129
|
+
#
|
130
|
+
# === Parameters
|
131
|
+
# cmd(String):: Name of the command to run
|
132
|
+
# arg1(String):: Optional, first command-line argumument
|
133
|
+
# arg2(String):: Optional, first command-line argumument
|
134
|
+
# ...
|
135
|
+
# argN(String):: Optional, Nth command-line argumument
|
136
|
+
#
|
137
|
+
# === Return
|
138
|
+
# output(String):: The process's output
|
139
|
+
# status(Process::Status):: The process's exit status
|
140
|
+
def self.run(cmd, *args)
|
141
|
+
pm = ProcessWatcher::ProcessMonitor.new
|
142
|
+
status = nil
|
143
|
+
output = StringIO.new
|
144
|
+
|
145
|
+
pm.spawn(cmd, *args) do |options|
|
146
|
+
output << options[:output] if options[:output]
|
147
|
+
status = options[:exit_status] if options[:exit_status]
|
148
|
+
end
|
149
|
+
|
150
|
+
pm.cleanup
|
151
|
+
output.close
|
152
|
+
output = output.string
|
153
|
+
return [output, status]
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module ProcessWatcher
|
25
|
+
# *nix specific watcher implementation
|
26
|
+
class ProcessMonitor
|
27
|
+
# Spawn given process and callback given block with output and exit code. This method
|
28
|
+
# accepts a variable number of parameters; the first param is always the command to
|
29
|
+
# run; successive parameters are command-line arguments for the process.
|
30
|
+
#
|
31
|
+
# === Parameters
|
32
|
+
# cmd(String):: Name of the command to run
|
33
|
+
# arg1(String):: Optional, first command-line argumument
|
34
|
+
# arg2(String):: Optional, first command-line argumument
|
35
|
+
# ...
|
36
|
+
# argN(String):: Optional, Nth command-line argumument
|
37
|
+
#
|
38
|
+
# === Block
|
39
|
+
# Given block should take one argument which is a hash which may contain
|
40
|
+
# the keys :output and :exit_code. The value associated with :output is a chunk
|
41
|
+
# of output while the value associated with :exit_code is the process exit code
|
42
|
+
# This block won't be called anymore once the :exit_code key has associated value
|
43
|
+
#
|
44
|
+
# === Return
|
45
|
+
# pid(Integer):: Spawned process pid
|
46
|
+
def spawn(cmd, *args)
|
47
|
+
args = args.map { |a| a.to_s } #exec only likes string arguments
|
48
|
+
|
49
|
+
#Run subprocess; capture its output using a pipe
|
50
|
+
pr, pw = IO::pipe
|
51
|
+
@pid = fork do
|
52
|
+
oldstderr = STDERR.clone
|
53
|
+
pr.close
|
54
|
+
STDIN.reopen(File.open('/dev/null', 'r'))
|
55
|
+
STDOUT.reopen(pw)
|
56
|
+
STDERR.reopen(pw)
|
57
|
+
begin
|
58
|
+
exec(cmd, *args)
|
59
|
+
rescue
|
60
|
+
oldstderr.puts "Couldn't exec: #{$!}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#Monitor subprocess output and status in a dedicated thread
|
65
|
+
pw.close
|
66
|
+
@io = pr
|
67
|
+
@reader = Thread.new do
|
68
|
+
status = nil
|
69
|
+
loop do
|
70
|
+
status = Process.waitpid(@pid, Process::WNOHANG)
|
71
|
+
break unless $?.nil?
|
72
|
+
array = select([@io], nil, [@io], 0.1)
|
73
|
+
array[0].each do |fdes|
|
74
|
+
unless fdes.eof?
|
75
|
+
# HACK HACK HACK 4096 is a magic number I pulled out of my
|
76
|
+
# ass, the real one should depend on the kernel's buffer
|
77
|
+
# sizes.
|
78
|
+
result = fdes.readpartial(4096)
|
79
|
+
yield(:output => result)
|
80
|
+
end
|
81
|
+
end unless array.nil?
|
82
|
+
array[2].each do |fdes|
|
83
|
+
# Do something with erroneous condition.
|
84
|
+
end unless array.nil?
|
85
|
+
end
|
86
|
+
yield(:exit_code => $?.exitstatus, :exit_status => $?)
|
87
|
+
end
|
88
|
+
|
89
|
+
return @pid
|
90
|
+
end
|
91
|
+
|
92
|
+
# Close io and join reader thread
|
93
|
+
#
|
94
|
+
# === Return
|
95
|
+
# true:: Always return true
|
96
|
+
def cleanup
|
97
|
+
@reader.join
|
98
|
+
@io.close
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'find'
|
25
|
+
if RUBY_PLATFORM =~ /mswin/
|
26
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'win32', 'process_monitor'))
|
27
|
+
else
|
28
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'linux', 'process_monitor'))
|
29
|
+
end
|
30
|
+
|
31
|
+
module ProcessWatcher
|
32
|
+
# Encapsulate information returned by watcher
|
33
|
+
class WatchStatus
|
34
|
+
|
35
|
+
# Potential outcome of watcher
|
36
|
+
VALID_STATUSES = [ :success, :timeout, :size_exceeded ]
|
37
|
+
|
38
|
+
attr_reader :status # One of VALID_STATUSES
|
39
|
+
attr_reader :exit_code # Watched process exit code or -1 if process was killed
|
40
|
+
attr_reader :output # Watched process combined output
|
41
|
+
|
42
|
+
# Initialize attibutes
|
43
|
+
def initialize(status, exit_code, output)
|
44
|
+
@status = status
|
45
|
+
@exit_code = exit_code
|
46
|
+
@output = output
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class Watcher
|
52
|
+
|
53
|
+
attr_reader :max_bytes # Maximum size in bytes of watched directory before process is killed
|
54
|
+
attr_reader :max_seconds # Maximum number of elapased seconds before external process is killed
|
55
|
+
|
56
|
+
# Initialize attributes
|
57
|
+
#
|
58
|
+
# max_bytes(Integer):: Maximum size in bytes of watched directory before process is killed
|
59
|
+
# max_seconds(Integer):: Maximum number of elapased seconds before external process is killed
|
60
|
+
def initialize(max_bytes, max_seconds)
|
61
|
+
@max_bytes = max_bytes
|
62
|
+
@max_seconds = max_seconds
|
63
|
+
end
|
64
|
+
|
65
|
+
# Launch given command as external process and watch given directory
|
66
|
+
# so it doesn't exceed given size. Also watch time elapsed and kill
|
67
|
+
# external process if either the size of the watched directory exceed
|
68
|
+
# @max_bytes or the time elapsed exceeds @max_seconds.
|
69
|
+
# Note: This method is not thread-safe, instantiate one watcher per thread
|
70
|
+
#
|
71
|
+
# === Parameters
|
72
|
+
# cmd(String):: command to run
|
73
|
+
# args(Array):: arguments for +cmd+
|
74
|
+
# dest_dir(String):: Watched directory
|
75
|
+
#
|
76
|
+
# === Return
|
77
|
+
# res(RightScale::WatchStatus):: Outcome of watch, see RightScale::WatchStatus
|
78
|
+
def launch_and_watch(cmd, args, dest_dir)
|
79
|
+
exit_code = nil
|
80
|
+
output = ''
|
81
|
+
monitor = ProcessMonitor.new
|
82
|
+
|
83
|
+
# Run external process and monitor it in a new thread, platform specific
|
84
|
+
pid = monitor.spawn(cmd, *args) do |data|
|
85
|
+
output << data[:output] if data[:output]
|
86
|
+
exit_code = data[:exit_code] if data.include?(:exit_code)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Loop until process is done or times out or takes too much space
|
90
|
+
timed_out = repeat(1, @max_seconds) do
|
91
|
+
if @max_bytes < 0
|
92
|
+
exit_code
|
93
|
+
else
|
94
|
+
size = 0
|
95
|
+
Find.find(dest_dir) { |f| size += File.stat(f).size rescue 0 if File.file?(f) } if File.directory?(dest_dir)
|
96
|
+
size > @max_bytes || exit_code
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Cleanup and report status
|
101
|
+
# Note: We need to store the exit status before we kill the underlying process so that
|
102
|
+
# if it finished in the mean time we still report -1 as exit code
|
103
|
+
if exit_code
|
104
|
+
exit_status = exit_code
|
105
|
+
outcome = :success
|
106
|
+
else
|
107
|
+
exit_status = -1
|
108
|
+
outcome = (timed_out ? :timeout : :size_exceeded)
|
109
|
+
Process.kill('INT', pid)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Cleanup any open handle etc., platform specific
|
113
|
+
monitor.cleanup
|
114
|
+
|
115
|
+
res = WatchStatus.new(outcome, exit_status, output)
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
# Run given block in thread and time execution
|
121
|
+
#
|
122
|
+
# === Block
|
123
|
+
# Block whose execution is timed
|
124
|
+
#
|
125
|
+
# === Return
|
126
|
+
# elapsed(Integer):: Number of seconds elapsed while running given block
|
127
|
+
def timed
|
128
|
+
start_at = Time.now
|
129
|
+
yield
|
130
|
+
elapsed = Time.now - start_at
|
131
|
+
end
|
132
|
+
|
133
|
+
# Repeat given block at regular intervals
|
134
|
+
#
|
135
|
+
# === Parameters
|
136
|
+
# seconds(Integer):: Number of seconds between executions
|
137
|
+
# timeout(Integer):: Timeout after which execution stops and method returns
|
138
|
+
#
|
139
|
+
# === Block
|
140
|
+
# Given block gets executed every period seconds until timeout is reached
|
141
|
+
# *or* block returns true
|
142
|
+
#
|
143
|
+
# === Return
|
144
|
+
# res(TrueClass|FalseClass):: true if timeout is reached, false otherwise.
|
145
|
+
def repeat(period, timeout)
|
146
|
+
end_at = Time.now + timeout
|
147
|
+
while res = (timeout < 0 || Time.now < end_at)
|
148
|
+
exit = false
|
149
|
+
elapsed = timed { exit = yield }
|
150
|
+
break if exit
|
151
|
+
sleep(period - elapsed) if elapsed < period
|
152
|
+
end
|
153
|
+
!res
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'win32/process'
|
25
|
+
|
26
|
+
module ProcessWatcher
|
27
|
+
# Windows specific watcher implementation
|
28
|
+
class ProcessMonitor
|
29
|
+
|
30
|
+
include ::Windows::Process
|
31
|
+
include ::Windows::Synchronize
|
32
|
+
include ::Windows::Handle
|
33
|
+
|
34
|
+
# Spawn given process and callback given block with output and exit code
|
35
|
+
#
|
36
|
+
# === Parameters
|
37
|
+
# cmd(String):: Process command line (including arguments)
|
38
|
+
# arg1(String):: Optional, first command-line argumument
|
39
|
+
# arg2(String):: Optional, first command-line argumument
|
40
|
+
# ...
|
41
|
+
# argN(String):: Optional, Nth command-line argumument
|
42
|
+
#
|
43
|
+
# === Block
|
44
|
+
# Given block should take one argument which is a hash which may contain
|
45
|
+
# the keys :output and :exit_code. The value associated with :output is a chunk
|
46
|
+
# of output while the value associated with :exit_code is the process exit code
|
47
|
+
# This block won't be called anymore once the :exit_code key has associated value
|
48
|
+
#
|
49
|
+
# === Return
|
50
|
+
# pid(Integer):: Spawned process pid
|
51
|
+
def spawn(cmd, *args)
|
52
|
+
args = args.map { |a| a.to_s }
|
53
|
+
cmd = ([cmd] + args).join(' ')
|
54
|
+
|
55
|
+
# Run external process and monitor it in a new thread
|
56
|
+
@io = IO.popen(cmd)
|
57
|
+
@handle = OpenProcess(PROCESS_ALL_ACCESS, 0, @io.pid)
|
58
|
+
case @handle
|
59
|
+
when INVALID_HANDLE_VALUE
|
60
|
+
# Something bad happened
|
61
|
+
yield(:exit_code => 1)
|
62
|
+
when 0
|
63
|
+
# Process already finished
|
64
|
+
yield(:exit_code => 0)
|
65
|
+
else
|
66
|
+
# Start output read
|
67
|
+
@reader = Thread.new do
|
68
|
+
o = @io.read
|
69
|
+
until o == ''
|
70
|
+
yield(:output => o)
|
71
|
+
o = @io.read
|
72
|
+
end
|
73
|
+
status = WaitForSingleObject(@handle, INFINITE)
|
74
|
+
exit_code = [0].pack('L')
|
75
|
+
if GetExitCodeProcess(@handle, exit_code)
|
76
|
+
exit_code = exit_code.unpack('L').first
|
77
|
+
else
|
78
|
+
exit_code = 1
|
79
|
+
end
|
80
|
+
yield(:exit_code => exit_code)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@io.pid
|
84
|
+
end
|
85
|
+
|
86
|
+
# Cleanup underlying handle
|
87
|
+
#
|
88
|
+
# === Return
|
89
|
+
# true:: Always return true
|
90
|
+
def cleanup
|
91
|
+
@reader.join
|
92
|
+
CloseHandle(@handle) if @handle > 0
|
93
|
+
@io.close
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "process_watcher/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "process_watcher"
|
7
|
+
s.version = ProcessWatcher::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Graham Hughes", "Raphael Simon"]
|
10
|
+
s.email = ["graham@rightscale.com"]
|
11
|
+
s.homepage = "http://rubygems.org/gems/process_watcher"
|
12
|
+
s.summary = %q{Cross platform interface to running subprocesses}
|
13
|
+
s.description = <<-EOF
|
14
|
+
ProcessWatcher is a cross platform interface for running subprocesses
|
15
|
+
safely. Unlike backticks or popen in Ruby 1.8, it will not invoke a
|
16
|
+
shell. Unlike system, it will permits capturing the output. Unlike
|
17
|
+
rolling it by hand, it runs on Windows.
|
18
|
+
EOF
|
19
|
+
|
20
|
+
s.rubyforge_project = "process_watcher"
|
21
|
+
|
22
|
+
s.requirements << 'win32-process ~> 0.6.1 gem on Windows systems'
|
23
|
+
s.add_development_dependency('rspec', "~> 1.3")
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
25
|
+
|
26
|
+
describe ProcessWatcher::Watcher do
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
@dest_dir = File.join(File.dirname(__FILE__), '__destdir')
|
30
|
+
FileUtils.mkdir_p(@dest_dir)
|
31
|
+
end
|
32
|
+
|
33
|
+
after(:each) do
|
34
|
+
FileUtils.rm_rf(@dest_dir)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should launch and watch well-behaved processes' do
|
38
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes=1, max_seconds=5)
|
39
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 42"
|
40
|
+
status = watcher.launch_and_watch('ruby', ['-e', ruby], @dest_dir)
|
41
|
+
status.status.should == :success
|
42
|
+
status.exit_code.should == 42
|
43
|
+
status.output.should == "42\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should report timeouts' do
|
47
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes=1, max_seconds=2)
|
48
|
+
ruby = "trap('INT', 'IGNORE'); STDOUT.sync = true; puts 42; sleep 5"
|
49
|
+
status = watcher.launch_and_watch('ruby', ['-e', ruby], @dest_dir)
|
50
|
+
status.status.should == :timeout
|
51
|
+
status.exit_code.should == -1
|
52
|
+
status.output.should == "42\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should report size exceeded' do
|
56
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes=1, max_seconds=5)
|
57
|
+
ruby = "trap('INT', 'IGNORE'); STDOUT.sync = true; puts 42; File.open(File.join('#{@dest_dir}', 'test'), 'w') { |f| f.puts 'MORE THAN 2 CHARS' }; sleep 5 rescue nil"
|
58
|
+
status = watcher.launch_and_watch('ruby', ['-e', ruby], @dest_dir)
|
59
|
+
status.status.should == :size_exceeded
|
60
|
+
status.exit_code.should == -1
|
61
|
+
status.output.should == "42\n"
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should allow infinite size and timeout' do
|
65
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes=-1, max_seconds=-1)
|
66
|
+
ruby = "trap('INT', 'IGNORE'); STDOUT.sync = true; puts 42; File.open(File.join('#{@dest_dir}', 'test'), 'w') { |f| f.puts 'MORE THAN 2 CHARS' }; sleep 2 rescue nil"
|
67
|
+
status = watcher.launch_and_watch('ruby', ['-e', ruby], @dest_dir)
|
68
|
+
status.status.should == :success
|
69
|
+
status.exit_code.should == 0
|
70
|
+
status.output.should == "42\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should permit array arguments' do
|
74
|
+
watcher = ProcessWatcher::Watcher.new(max_bytes=-1, max_seconds=-1)
|
75
|
+
status = watcher.launch_and_watch("echo", ["$HOME", ";", "echo", "broken"], @dest_dir)
|
76
|
+
status.status.should == :success
|
77
|
+
status.exit_code.should == 0
|
78
|
+
status.output.should == "$HOME ; echo broken\n"
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
25
|
+
|
26
|
+
describe ProcessWatcher do
|
27
|
+
before(:each) do
|
28
|
+
@dest_dir = File.join(File.dirname(__FILE__), '__destdir')
|
29
|
+
FileUtils.mkdir_p(@dest_dir)
|
30
|
+
end
|
31
|
+
|
32
|
+
after(:each) do
|
33
|
+
FileUtils.rm_rf(@dest_dir)
|
34
|
+
end
|
35
|
+
|
36
|
+
context :watch do
|
37
|
+
it 'should launch and watch well-behaved processes' do
|
38
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 0"
|
39
|
+
ProcessWatcher.watch("ruby", ["-e", ruby]).should == "42\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should call the block as appropriate' do
|
43
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 0"
|
44
|
+
times = 0
|
45
|
+
ProcessWatcher.watch("ruby", ["-e", ruby], @dest_dir) { |type, text|
|
46
|
+
case times
|
47
|
+
when 0 then type.should == :begin
|
48
|
+
when 1 then type.should == :commit
|
49
|
+
else fail_with("callback should only be called twice", nil, times)
|
50
|
+
end
|
51
|
+
times += 1
|
52
|
+
text.should =~ /^in #{Regexp.escape(@dest_dir)}, running ruby -e #{Regexp.escape(Shellwords.escape(ruby))}/
|
53
|
+
}.should == "42\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should report weird error codes' do
|
57
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 42"
|
58
|
+
lambda {
|
59
|
+
ProcessWatcher.watch("ruby", ["-e", ruby], @dest_dir, 1, 2)
|
60
|
+
}.should raise_exception(ProcessWatcher::NonzeroExitCode) {|e| e.exit_code.should == 42}
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should report timeouts' do
|
64
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; sleep 5"
|
65
|
+
lambda {
|
66
|
+
ProcessWatcher.watch("ruby", ["-e", ruby], @dest_dir, 1, 2)
|
67
|
+
}.should raise_exception(ProcessWatcher::TimeoutError)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should report size exceeded' do
|
71
|
+
ruby = "trap('INT', 'IGNORE'); STDOUT.sync = true; puts 42; File.open" +
|
72
|
+
"(File.join('#{@dest_dir}', 'test'), 'w') { |f| f.puts 'MORE THAN 2 CHARS' }; sleep 5 rescue nil"
|
73
|
+
lambda {
|
74
|
+
ProcessWatcher.watch("ruby", ["-e", ruby], @dest_dir, 1, -1)
|
75
|
+
}.should raise_exception(ProcessWatcher::TooMuchSpaceError)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should allow infinite size and timeout' do
|
79
|
+
ruby = "trap('INT', 'IGNORE'); STDOUT.sync = true; puts 42; " +
|
80
|
+
"File.open(File.join('#{@dest_dir}', 'test'), 'w') { |f| " +
|
81
|
+
"f.puts 'MORE THAN 2 CHARS' }; sleep 2 rescue nil"
|
82
|
+
ProcessWatcher.watch("ruby", ["-e", ruby]).should == "42\n"
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should permit array arguments' do
|
86
|
+
ProcessWatcher.watch("echo", ["$HOME", ";", "echo", "broken"]).should == "$HOME ; echo broken\n"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context :run do
|
91
|
+
it 'should launch well-behaved processes' do
|
92
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 0"
|
93
|
+
output, status = ProcessWatcher.run("ruby", "-e", ruby)
|
94
|
+
output.should == "42\n"
|
95
|
+
status.exitstatus.should == 0
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should record weird error codes in $?' do
|
99
|
+
ruby = "trap('INT', 'IGNORE'); puts 42; exit 42"
|
100
|
+
output, status = ProcessWatcher.run("ruby", "-e", ruby)
|
101
|
+
output.should == "42\n"
|
102
|
+
status.exitstatus.should == 42
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should run in the current directory' do
|
106
|
+
ruby = "trap('INT', 'IGNORE'); puts File.expand_path('.')"
|
107
|
+
output, status = ProcessWatcher.run("ruby", "-e", ruby)
|
108
|
+
output.should == File.expand_path('.') + "\n"
|
109
|
+
status.exitstatus.should == 0
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/spec/rcov.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--exclude "spec/*"~
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
|
26
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'process_watcher'))
|
27
|
+
|
28
|
+
require 'spec'
|
29
|
+
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: process_watcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: "0.2"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Graham Hughes
|
13
|
+
- Raphael Simon
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-14 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
name: rspec
|
25
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 9
|
31
|
+
segments:
|
32
|
+
- 1
|
33
|
+
- 3
|
34
|
+
version: "1.3"
|
35
|
+
requirement: *id001
|
36
|
+
description: |
|
37
|
+
ProcessWatcher is a cross platform interface for running subprocesses
|
38
|
+
safely. Unlike backticks or popen in Ruby 1.8, it will not invoke a
|
39
|
+
shell. Unlike system, it will permits capturing the output. Unlike
|
40
|
+
rolling it by hand, it runs on Windows.
|
41
|
+
|
42
|
+
email:
|
43
|
+
- graham@rightscale.com
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
extra_rdoc_files: []
|
49
|
+
|
50
|
+
files:
|
51
|
+
- .gitignore
|
52
|
+
- Gemfile
|
53
|
+
- Gemfile.lock
|
54
|
+
- LICENSE
|
55
|
+
- README.rdoc
|
56
|
+
- Rakefile
|
57
|
+
- lib/process_watcher.rb
|
58
|
+
- lib/process_watcher/linux/process_monitor.rb
|
59
|
+
- lib/process_watcher/version.rb
|
60
|
+
- lib/process_watcher/watcher.rb
|
61
|
+
- lib/process_watcher/win32/process_monitor.rb
|
62
|
+
- process_watcher.gemspec
|
63
|
+
- spec/process_watcher/watcher_spec.rb
|
64
|
+
- spec/process_watcher_spec.rb
|
65
|
+
- spec/rcov.opts
|
66
|
+
- spec/spec.opts
|
67
|
+
- spec/spec_helper.rb
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: http://rubygems.org/gems/process_watcher
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
requirements:
|
96
|
+
- win32-process ~> 0.6.1 gem on Windows systems
|
97
|
+
rubyforge_project: process_watcher
|
98
|
+
rubygems_version: 1.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: Cross platform interface to running subprocesses
|
102
|
+
test_files:
|
103
|
+
- spec/process_watcher/watcher_spec.rb
|
104
|
+
- spec/process_watcher_spec.rb
|
105
|
+
- spec/rcov.opts
|
106
|
+
- spec/spec.opts
|
107
|
+
- spec/spec_helper.rb
|