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 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