right_popen 1.0.11 → 1.0.16

Sign up to get free protection for your applications and to get access to all the features.
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