right_popen 1.0.11 → 1.0.16

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/Rakefile CHANGED
@@ -1,14 +1,37 @@
1
1
  require 'rubygems'
2
+ require 'bundler'
2
3
  require 'fileutils'
3
4
  require 'rake'
4
5
  require 'rake/clean'
5
- require 'rake/testtask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'spec/rake/spectask'
6
9
  require 'rbconfig'
7
10
 
11
+ def list_spec_files
12
+ list = Dir['spec/**/*_spec.rb']
13
+ list.delete_if { |path| path.include?('/linux/') } if RUBY_PLATFORM =~ /mswin/
14
+ list
15
+ end
16
+
8
17
  include Config
9
18
 
19
+ Bundler::GemHelper.install_tasks
20
+
21
+ # == Gem == #
22
+
23
+ gemtask = Rake::GemPackageTask.new(Gem::Specification.load("right_popen.gemspec")) do |package|
24
+ package.package_dir = ENV['PACKAGE_DIR'] || 'pkg'
25
+ package.need_zip = true
26
+ package.need_tar = true
27
+ end
28
+
29
+ directory gemtask.package_dir
30
+
31
+ CLEAN.include(gemtask.package_dir)
32
+
10
33
  desc "Clean any build files for right_popen"
11
- task :clean do
34
+ task :win_clean do
12
35
  if RUBY_PLATFORM =~ /mswin/
13
36
  if File.exists?('ext/Makefile')
14
37
  Dir.chdir('ext') do
@@ -18,6 +41,7 @@ task :clean do
18
41
  rm 'lib/win32/right_popen.so' if File.file?('lib/win32/right_popen.so')
19
42
  end
20
43
  end
44
+ task :clean => :win_clean
21
45
 
22
46
  desc "Build right_popen (but don't install it)"
23
47
  task :build => [:clean] do
@@ -32,15 +56,14 @@ task :build => [:clean] do
32
56
  end
33
57
 
34
58
  desc "Build a binary gem"
35
- task :gem => [:build] do
36
- Dir["*.gem"].each { |gem| rm gem }
37
- ruby 'right_popen.gemspec'
38
- end
59
+ task :gem => [:build]
39
60
 
40
61
  desc 'Install the right_popen library as a gem'
41
62
  task :install_gem => [:gem] do
42
- file = Dir["*.gem"].first
43
- sh "gem install #{file}"
63
+ Dir.chdir(File.dirname(__FILE__)) do
64
+ file = Dir["pkg/*.gem"].first
65
+ sh "gem install #{file}"
66
+ end
44
67
  end
45
68
 
46
69
  desc 'Uninstalls and reinstalls the right_popen library as a gem'
@@ -49,7 +72,44 @@ task :reinstall_gem do
49
72
  sh "rake install_gem"
50
73
  end
51
74
 
52
- desc 'Runs all spec tests'
53
- task :spec do
54
- sh "spec spec/*_spec.rb"
75
+ # == Unit Tests == #
76
+
77
+ task :specs => :spec
78
+
79
+ desc "Run unit tests"
80
+ Spec::Rake::SpecTask.new do |t|
81
+ t.spec_files = list_spec_files
82
+ end
83
+
84
+ desc "Run unit tests with RCov"
85
+ Spec::Rake::SpecTask.new(:rcov) do |t|
86
+ t.spec_files = list_spec_files
87
+ t.rcov = true
88
+ end
89
+
90
+ desc "Print Specdoc for unit tests"
91
+ Spec::Rake::SpecTask.new(:doc) do |t|
92
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
93
+ t.spec_files = list_spec_files
94
+ end
95
+
96
+ # == Documentation == #
97
+
98
+ desc "Generate API documentation to doc/rdocs/index.html"
99
+ Rake::RDocTask.new do |rd|
100
+ rd.rdoc_dir = 'doc/rdocs'
101
+ rd.main = 'README.rdoc'
102
+ rd.rdoc_files.include 'README.rdoc', "lib/**/*.rb"
103
+
104
+ rd.options << '--inline-source'
105
+ rd.options << '--line-numbers'
106
+ rd.options << '--all'
107
+ rd.options << '--fileboxes'
108
+ rd.options << '--diagram'
109
+ end
110
+
111
+ # == Emacs integration == #
112
+ desc "Rebuild TAGS file"
113
+ task :tags do
114
+ sh "rtags -R lib spec"
55
115
  end
data/lib/right_popen.rb CHANGED
@@ -26,9 +26,9 @@
26
26
  # It relies on EventMachine for most of its internal mechanisms.
27
27
 
28
28
  if RUBY_PLATFORM =~ /mswin/
29
- require File.expand_path(File.join(File.dirname(__FILE__), 'win32', 'right_popen'))
29
+ require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'win32', 'right_popen'))
30
30
  else
31
- require File.expand_path(File.join(File.dirname(__FILE__), 'linux', 'right_popen'))
31
+ require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'linux', 'right_popen'))
32
32
  end
33
33
 
34
34
  module RightScale
@@ -55,13 +55,13 @@ module RightScale
55
55
  # options[:exit_handler](String):: Exit handler method name, optional
56
56
  #
57
57
  # === Returns
58
- # true:: Always returns true
58
+ # true:: always true
59
59
  def self.popen3(options)
60
60
  raise "EventMachine reactor must be started" unless EM.reactor_running?
61
61
  raise "Missing command" unless options[:command]
62
62
  raise "Missing target" unless options[:target] || !options[:stdout_handler] && !options[:stderr_handler] && !options[:exit_handler] && !options[:pid_handler]
63
- RightScale.popen3_imp(options)
64
- true
63
+ GC.start # To garbage collect open file descriptors from passed executions
64
+ return RightScale.popen3_imp(options)
65
65
  end
66
66
 
67
67
  end
@@ -0,0 +1,117 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 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 RightScale
25
+ module RightPopen
26
+ class Accumulator
27
+ READ_CHUNK_SIZE = 4096
28
+
29
+ def initialize(process, inputs, read_callbacks, outputs, write_callbacks)
30
+ @process = process
31
+ @inputs = inputs
32
+ @outputs = outputs
33
+ null = Proc.new {}
34
+ @reads = {}
35
+ @writes = {}
36
+ inputs.zip(read_callbacks).each do |pair|
37
+ input, callback = pair
38
+ @reads[input] = callback
39
+ end
40
+ outputs.zip(write_callbacks).each do |pair|
41
+ output, callback = pair
42
+ @writes[output] = callback
43
+ end
44
+ @writebuffers = {}
45
+ @status = nil
46
+ end
47
+
48
+ def tick(sleep_time = 0.1)
49
+ return true unless @process.status.nil?
50
+
51
+ @process.status = status = ::Process.waitpid2(@process.pid, ::Process::WNOHANG)
52
+
53
+ inputs = @inputs.dup
54
+ outputs = @outputs.dup
55
+ ready = nil
56
+ while ready.nil?
57
+ begin
58
+ # in theory, we should note "exceptional conditions" and
59
+ # permit procs for those, too. In practice there are only
60
+ # two times when exceptional conditions occur: out of band
61
+ # data in TCP connections and "packet mode" for
62
+ # pseudoterminals. We care about neither of these,
63
+ # therefore ignore exceptional conditions.
64
+ ready = IO.select(inputs, outputs, nil, sleep_time)
65
+ rescue Errno::EAGAIN, Errno::EINTR
66
+ end
67
+ end unless inputs.empty? && outputs.empty?
68
+
69
+ ready[0].each do |fdes|
70
+ if fdes.eof?
71
+ fdes.close
72
+ @inputs.delete(fdes)
73
+ else
74
+ chunk = fdes.readpartial(READ_CHUNK_SIZE)
75
+ @reads[fdes].call(chunk) if @reads[fdes]
76
+ end
77
+ end unless ready.nil? || ready[0].nil?
78
+ ready[1].each do |fdes|
79
+ buffered = @writebuffers[fdes]
80
+ buffered = @writes[fdes].call if @writes[fdes] if buffered.nil? || buffered.empty?
81
+ if buffered.nil?
82
+ fdes.close
83
+ @outputs.delete(fdes)
84
+ elsif !buffered.empty?
85
+ begin
86
+ amount = fdes.write_nonblock buffered
87
+ @writebuffers[fdes] = buffered[amount..-1]
88
+ rescue Errno::EPIPE
89
+ # subprocess closed the pipe; fine.
90
+ fdes.close
91
+ @outputs.delete(fdes)
92
+ end
93
+ end
94
+ end unless ready.nil? || ready[1].nil?
95
+
96
+ return !@process.status.nil?
97
+ end
98
+
99
+ def number_waiting_on
100
+ @inputs.size + @outputs.size
101
+ end
102
+
103
+ def cleanup
104
+ @inputs.each {|p| p.close unless p.closed? }
105
+ @outputs.each {|p| p.close unless p.closed? }
106
+ @process.status = ::Process.waitpid2(@process.pid) if @process.status.nil?
107
+ end
108
+
109
+ def run_to_completion(sleep_time=0.1)
110
+ until tick(sleep_time)
111
+ break if number_waiting_on == 0
112
+ end
113
+ cleanup
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,148 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 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 'etc'
25
+
26
+ module RightScale
27
+ module RightPopen
28
+ class Process
29
+ attr_reader :pid, :stdin, :stdout, :stderr, :status_fd
30
+ attr_accessor :status
31
+
32
+ def initialize(parameters={})
33
+ parameters[:locale] = true unless parameters.has_key?(:locale)
34
+ @parameters = parameters
35
+ @status_fd = nil
36
+ end
37
+
38
+ def fork(cmd)
39
+ @cmd = cmd
40
+ stdin_r, stdin_w = IO.pipe
41
+ stdout_r, stdout_w = IO.pipe
42
+ stderr_r, stderr_w = IO.pipe
43
+ status_r, status_w = IO.pipe
44
+
45
+ [stdin_r, stdin_w, stdout_r, stdout_w,
46
+ stderr_r, stderr_w, status_r, status_w].each {|fdes| fdes.sync = true}
47
+
48
+ @pid = Kernel::fork do
49
+ begin
50
+ stdin_w.close
51
+ STDIN.reopen stdin_r
52
+
53
+ stdout_r.close
54
+ STDOUT.reopen stdout_w
55
+
56
+ stderr_r.close
57
+ STDERR.reopen stderr_w
58
+
59
+ status_r.close
60
+ status_w.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
61
+
62
+ if group = get_group
63
+ ::Process.egid = group
64
+ ::Process.gid = group
65
+ end
66
+
67
+ if user = get_user
68
+ ::Process.euid = user
69
+ ::Process.uid = user
70
+ end
71
+
72
+ Dir.chdir(@parameters[:directory]) if @parameters[:directory]
73
+
74
+ ENV["LC_ALL"] = "C" if @parameters[:locale]
75
+
76
+ @parameters[:environment].each do |key,value|
77
+ ENV[key.to_s] = value.to_s
78
+ end if @parameters[:environment]
79
+
80
+ File.umask(get_umask) if @parameters[:umask]
81
+
82
+ if cmd.kind_of?(Array)
83
+ exec(*cmd)
84
+ else
85
+ exec("sh", "-c", cmd)
86
+ end
87
+ raise 'forty-two'
88
+ rescue Exception => e
89
+ Marshal.dump(e, status_w)
90
+ end
91
+ status_w.close
92
+ exit!
93
+ end
94
+
95
+ stdin_r.close
96
+ stdout_w.close
97
+ stderr_w.close
98
+ status_w.close
99
+ @stdin = stdin_w
100
+ @stdout = stdout_r
101
+ @stderr = stderr_r
102
+ @status_fd = status_r
103
+ end
104
+
105
+ def wait_for_exec
106
+ begin
107
+ e = Marshal.load @status_fd
108
+ # thus meaning that the process failed to exec...
109
+ @stdin.close
110
+ @stdout.close
111
+ @stderr.close
112
+ raise(Exception === e ? e : "unknown failure!")
113
+ rescue EOFError
114
+ # thus meaning that the process did exec and we can continue.
115
+ ensure
116
+ @status_fd.close
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def get_user
123
+ user = @parameters[:user] || nil
124
+ unless user.kind_of?(Integer)
125
+ user = Etc.getpwnam(user).uid if user
126
+ end
127
+ user
128
+ end
129
+
130
+ def get_group
131
+ group = @parameters[:group] || nil
132
+ unless group.kind_of?(Integer)
133
+ group = Etc.getgrnam(group).gid if group
134
+ end
135
+ group
136
+ end
137
+
138
+ def get_umask
139
+ if @parameters[:umask].respond_to?(:oct)
140
+ value = @parameters[:umask].oct
141
+ else
142
+ value = @parameters[:umask].to_i
143
+ end
144
+ value & 007777
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,170 @@
1
+ #--
2
+ # Copyright (c) 2009 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
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ # RightScale.popen3 allows running external processes aynchronously
25
+ # while still capturing their standard and error outputs.
26
+ # It relies on EventMachine for most of its internal mechanisms.
27
+
28
+ require 'rubygems'
29
+ require 'eventmachine'
30
+ require File.expand_path(File.join(File.dirname(__FILE__), "process"))
31
+ require File.expand_path(File.join(File.dirname(__FILE__), "accumulator"))
32
+ require File.expand_path(File.join(File.dirname(__FILE__), "utilities"))
33
+
34
+ module RightScale
35
+ # ensure uniqueness of handler to avoid confusion.
36
+ raise "#{StatusHandler.name} is already defined" if defined?(StatusHandler)
37
+
38
+ module StatusHandler
39
+ def initialize(file_handle)
40
+ # Voodoo to make sure that Ruby doesn't gc the file handle
41
+ # (closing the stream) before we're done with it. No, oddly
42
+ # enough EventMachine is not good about holding on to this
43
+ # itself.
44
+ @handle = file_handle
45
+ @data = ""
46
+ end
47
+
48
+ def receive_data(data)
49
+ @data << data
50
+ end
51
+
52
+ def drain_and_close
53
+ begin
54
+ while ready = IO.select([@handle], nil, nil, 0)
55
+ break if @handle.eof?
56
+ data = @handle.readpartial(4096)
57
+ receive_data(data)
58
+ end
59
+ rescue Errno::EBADF
60
+ rescue EOFError
61
+ end
62
+ close_connection
63
+ end
64
+
65
+ def unbind
66
+ if @data.size > 0
67
+ e = Marshal.load @data
68
+ raise (Exception === e ? e : "unknown failure: saw #{e} on status channel")
69
+ end
70
+ end
71
+ end
72
+
73
+ # ensure uniqueness of handler to avoid confusion.
74
+ raise "#{PipeHandler.name} is already defined" if defined?(PipeHandler)
75
+
76
+ module PipeHandler
77
+ def initialize(file_handle, target, handler)
78
+ # Voodoo to make sure that Ruby doesn't gc the file handle
79
+ # (closing the stream) before we're done with it. No, oddly
80
+ # enough EventMachine is not good about holding on to this
81
+ # itself.
82
+ @handle = file_handle
83
+ @target = target
84
+ @handler = handler
85
+ end
86
+
87
+ def receive_data(data)
88
+ @target.method(@handler).call(data) if @handler
89
+ end
90
+
91
+ def drain_and_close
92
+ begin
93
+ while ready = IO.select([@handle], nil, nil, 0)
94
+ break if @handle.eof?
95
+ data = @handle.readpartial(4096)
96
+ receive_data(data)
97
+ end
98
+ rescue Errno::EBADF
99
+ rescue EOFError
100
+ end
101
+ close_connection
102
+ end
103
+ end
104
+
105
+ # ensure uniqueness of handler to avoid confusion.
106
+ raise "#{InputHandler.name} is already defined" if defined?(InputHandler)
107
+
108
+ module InputHandler
109
+ def initialize(file_handle, string)
110
+ # Voodoo to make sure that Ruby doesn't gc the file handle
111
+ # (closing the stream) before we're done with it. No, oddly
112
+ # enough EventMachine is not good about holding on to this
113
+ # itself.
114
+ @handle = file_handle
115
+ @string = string
116
+ end
117
+
118
+ def post_init
119
+ send_data(@string) if @string
120
+ close_connection_after_writing
121
+ end
122
+
123
+ def drain_and_close
124
+ close_connection
125
+ end
126
+ end
127
+
128
+ # Forks process to run given command asynchronously, hooking all three
129
+ # standard streams of the child process.
130
+ #
131
+ # === Parameters
132
+ # options[:pid_handler](Symbol):: Token for pid handler method name.
133
+ #
134
+ # See RightScale.popen3
135
+ def self.popen3_imp(options)
136
+ # note GC.start moved to common popen3 entry method for use by both windows and linux.
137
+ EM.next_tick do
138
+ process = RightPopen::Process.new(:environment => options[:environment] || {})
139
+ process.fork(options[:command])
140
+
141
+ status_handler = EM.attach(process.status_fd, StatusHandler, process.status_fd)
142
+ stderr_handler = EM.attach(process.stderr, PipeHandler, process.stderr, options[:target],
143
+ options[:stderr_handler])
144
+ stdout_handler = EM.attach(process.stdout, PipeHandler, process.stdout, options[:target],
145
+ options[:stdout_handler])
146
+ stdin_handler = EM.attach(process.stdin, InputHandler, process.stdin, options[:input])
147
+
148
+ options[:target].method(options[:pid_handler]).call(process.pid) if
149
+ options.key? :pid_handler
150
+
151
+ wait_timer = EM::PeriodicTimer.new(1) do
152
+ value = ::Process.waitpid2(process.pid, Process::WNOHANG)
153
+ unless value.nil?
154
+ begin
155
+ ignored, status = value
156
+ options[:target].method(options[:exit_handler]).call(status) if
157
+ options[:exit_handler]
158
+ ensure
159
+ stdin_handler.drain_and_close
160
+ stdout_handler.drain_and_close
161
+ stderr_handler.drain_and_close
162
+ status_handler.drain_and_close
163
+ wait_timer.cancel
164
+ end
165
+ end
166
+ end
167
+ end
168
+ true
169
+ end
170
+ end