process_watcher 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ doc
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in process_watcher.gemspec
4
+ gemspec
5
+
6
+ group :win32 do
7
+ gem "win32-process", "~> 0.6.1"
8
+ end
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,3 @@
1
+ module ProcessWatcher
2
+ VERSION = "0.2"
3
+ 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
@@ -0,0 +1,2 @@
1
+ --format=nested
2
+ --backtrace
@@ -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