process_watcher 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|