right-popen 1.0.0-x86-mswin32-60

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // 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
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
+ #include "ruby.h"
25
+ #include "rubysig.h"
26
+ #include "rubyio.h"
27
+
28
+ #include <malloc.h>
29
+ #include <io.h>
30
+ #include <string.h>
31
+ #include <fcntl.h>
32
+ #include <sys/stat.h>
@@ -0,0 +1,57 @@
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
+ if RUBY_PLATFORM =~ /mswin/
29
+ require File.expand_path(File.join(File.dirname(__FILE__), 'win32', 'right_popen'))
30
+ else
31
+ require File.expand_path(File.join(File.dirname(__FILE__), 'linux', 'right_popen'))
32
+ end
33
+
34
+ module RightScale
35
+
36
+ # Closes all open I/O objects in the object space, keeping any provided as
37
+ # arguments.
38
+ #
39
+ # === Parameters
40
+ # keep(Array):: array of I/O objects to keep.
41
+ def self.close_all_fd(keep = [])
42
+ keep = [keep] unless keep.is_a? Array
43
+ keep += [$stdin, $stdout, $stderr]
44
+ keep = keep.collect { |io| io.fileno }
45
+
46
+ ObjectSpace.each_object(IO) do |io|
47
+ unless io.closed? || keep.include?(io.fileno)
48
+ begin
49
+ io.close
50
+ rescue Exception
51
+ #do nothing
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,234 @@
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
+ begin
30
+ gem 'eventmachine', '=0.12.8.1' # patched version for Windows-only socket close fix
31
+ rescue Gem::LoadError
32
+ gem 'eventmachine', '=0.12.8' # notify_readable is deprecated in 0.12.10
33
+ end
34
+ require 'eventmachine'
35
+ require 'win32/process'
36
+
37
+ require File.join(File.dirname(__FILE__), 'right_popen.so') # win32 native code
38
+
39
+ module RightScale
40
+
41
+ # Provides an eventmachine callback handler for the stdout stream.
42
+ module StdOutHandler
43
+
44
+ # Quacks like Process::Status, which we cannot instantiate ourselves because
45
+ # has no public new method. RightScale::popen3 needs this because the
46
+ # 'win32/process' gem currently won't return Process::Status objects but
47
+ # only returns a [pid, exitstatus] value.
48
+ class Status
49
+ # Process ID
50
+ attr_reader :pid
51
+
52
+ # Process exit code
53
+ attr_reader :exitstatus
54
+
55
+ # === Parameters
56
+ # pid(Integer): Process ID.
57
+ #
58
+ # exitstatus(Integer): Process exit code
59
+ def initialize(pid, exitstatus)
60
+ @pid = pid
61
+ @exitstatus = exitstatus
62
+ end
63
+
64
+ # Simulates Process::Status.exited?
65
+ #
66
+ # === Returns
67
+ # true in all cases because this object cannot be returned until the
68
+ # process exits
69
+ def exited?
70
+ return true
71
+ end
72
+
73
+ # Simulates Process::Status.success?
74
+ #
75
+ # === Returns
76
+ # true in if the process returned zero as its exit code
77
+ def success?
78
+ return @exitstatus ? (0 == @exitstatus) : true;
79
+ end
80
+ end
81
+
82
+ # === Parameters
83
+ # target(Object): Object defining handler methods to be called.
84
+ #
85
+ # stdout_handler(String): Token for stdout handler method name.
86
+ #
87
+ # exit_handler(String): Token for exit handler method name.
88
+ #
89
+ # stderr_eventable(Connector): EM object representing stderr handler.
90
+ #
91
+ # stream_out(IO): Standard output stream.
92
+ #
93
+ # pid(Integer): Child process ID.
94
+ def initialize(target, stdout_handler, exit_handler, stderr_eventable, stream_out, pid)
95
+ @target = target
96
+ @stdout_handler = stdout_handler
97
+ @exit_handler = exit_handler
98
+ @stderr_eventable = stderr_eventable
99
+ @stream_out = stream_out
100
+ @pid = pid
101
+ @status = nil
102
+ end
103
+
104
+ # Callback from EM to asynchronously read the stdout stream. Note that this
105
+ # callback mechanism is deprecated after EM v0.12.8
106
+ def notify_readable
107
+ data = RightPopen.async_read(@stream_out)
108
+ receive_data(data) if (data && data.length > 0)
109
+ detach unless data
110
+ end
111
+
112
+ # Callback from EM to receive data, which we also use to handle the
113
+ # asynchronous data we read ourselves.
114
+ def receive_data(data)
115
+ @target.method(@stdout_handler).call(data) if @stdout_handler
116
+ end
117
+
118
+ # Override of Connection.get_status() for Windows implementation.
119
+ def get_status
120
+ unless @status
121
+ begin
122
+ @status = Status.new(@pid, Process.waitpid2(@pid)[1])
123
+ rescue Process::Error
124
+ # process is gone, which means we have no recourse to retrieve the
125
+ # actual exit code; let's be optimistic.
126
+ @status = Status.new(@pid, 0)
127
+ end
128
+ end
129
+ return @status
130
+ end
131
+
132
+ # Callback from EM to unbind.
133
+ def unbind
134
+ # We force the attached stderr handler to go away so that
135
+ # we don't end up with a broken pipe
136
+ @stderr_eventable.force_detach if @stderr_eventable
137
+ @target.method(@exit_handler).call(get_status) if @exit_handler
138
+ @stream_out.close
139
+ end
140
+ end
141
+
142
+ # Provides an eventmachine callback handler for the stderr stream.
143
+ module StdErrHandler
144
+
145
+ # === Parameters
146
+ # target(Object): Object defining handler methods to be called.
147
+ #
148
+ # stderr_handler(String): Token for stderr handler method name.
149
+ #
150
+ # stream_err(IO): Standard error stream.
151
+ def initialize(target, stderr_handler, stream_err)
152
+ @target = target
153
+ @stderr_handler = stderr_handler
154
+ @stream_err = stream_err
155
+ @unbound = false
156
+ end
157
+
158
+ # Callback from EM to asynchronously read the stderr stream. Note that this
159
+ # callback mechanism is deprecated after EM v0.12.8
160
+ def notify_readable
161
+ data = RightPopen.async_read(@stream_err)
162
+ receive_data(data) if (data && data.length > 0)
163
+ detach unless data
164
+ end
165
+
166
+ # Callback from EM to receive data, which we also use to handle the
167
+ # asynchronous data we read ourselves.
168
+ def receive_data(data)
169
+ @target.method(@stderr_handler).call(data)
170
+ end
171
+
172
+ # Callback from EM to unbind.
173
+ def unbind
174
+ @unbound = true
175
+ @stream_err.close
176
+ end
177
+
178
+ # Forces detachment of the stderr handler on EM's next tick.
179
+ def force_detach
180
+ # Use next tick to prevent issue in EM where descriptors list
181
+ # gets out-of-sync when calling detach in an unbind callback
182
+ EM.next_tick { detach unless @unbound }
183
+ end
184
+ end
185
+
186
+ # Creates a child process and connects event handlers to the standard output
187
+ # and error streams used by the created process. Connectors use named pipes
188
+ # and asynchronous I/O in the native Windows implementation.
189
+ #
190
+ # Streams the command's stdout and stderr to the given handlers. Time-
191
+ # ordering of bytes sent to stdout and stderr is not preserved.
192
+ #
193
+ # Calls given exit handler upon command process termination, passing in the
194
+ # resulting Process::Status.
195
+ #
196
+ # All handlers must be methods exposed by the given target.
197
+ #
198
+ # === Parameters
199
+ # cmd(String): command to execute, including any arguments.
200
+ #
201
+ # target(Object): object defining handler methods to be called.
202
+ #
203
+ # stdout_handler(String): token for stdout handler method name.
204
+ #
205
+ # stderr_handler(String): token for stderr handler method name.
206
+ #
207
+ # exit_handler(String): token for exit handler method name.
208
+ def self.popen3(cmd, target, stdout_handler = nil, stderr_handler = nil, exit_handler = nil)
209
+ raise "EventMachine reactor must be started" unless EM.reactor_running?
210
+
211
+ # launch cmd and request asynchronous output (which is only provided by
212
+ # the RightScale version of win32/open3 gem).
213
+ mode = "t"
214
+ show_window = false
215
+ asynchronous_output = true
216
+ stream_in, stream_out, stream_err, pid = RightPopen.popen4(cmd, mode, show_window, asynchronous_output)
217
+
218
+ # close input immediately.
219
+ stream_in.close
220
+
221
+ # attach handlers to event machine and let it monitor incoming data. the
222
+ # streams aren't used directly by the connectors except that they are closed
223
+ # on unbind.
224
+ stderr_eventable = EM.attach(stream_err, StdErrHandler, target, stderr_handler, stream_err) if stderr_handler
225
+ EM.attach(stream_out, StdOutHandler, target, stdout_handler, exit_handler, stderr_eventable, stream_out, pid)
226
+
227
+ # note that control returns to the caller, but the launched cmd continues
228
+ # running and sends output to the handlers. the caller is not responsible
229
+ # for waiting for the process to terminate or closing streams as the
230
+ # attached eventables will handle this automagically. notification will be
231
+ # sent to the exit_handler on process termination.
232
+ end
233
+
234
+ end
Binary file
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+
3
+ spec = Gem::Specification.new do |spec|
4
+ is_windows = RUBY_PLATFORM =~ /mswin/
5
+
6
+ spec.name = 'right-popen'
7
+ spec.version = '1.0.0'
8
+ spec.authors = ['Scott Messier', 'Raphael Simon']
9
+ spec.email = 'scott@rightscale.com'
10
+ spec.homepage = 'https://github.com/rightscale/right_popen'
11
+ if is_windows
12
+ spec.platform = 'x86-mswin32-60'
13
+ else
14
+ spec.platform = Gem::Platform::RUBY
15
+ end
16
+ spec.summary = 'Provides a platform-independent popen implementation'
17
+ spec.has_rdoc = true
18
+ spec.rdoc_options = ["--main", "README.rdoc", "--title", "RightPopen"]
19
+ spec.extra_rdoc_files = ["README.rdoc"]
20
+ spec.required_ruby_version = '>= 1.8.6'
21
+ spec.rubyforge_project = %q{right-popen}
22
+
23
+ spec.description = <<-EOF
24
+ RightPopen allows running external processes aynchronously while still
25
+ capturing their standard and error outputs. It relies on EventMachine for most
26
+ of its internal mechanisms. The linux implementation is valid for any linux
27
+ platform but there is also a native implementation for Windows platforms.
28
+ EOF
29
+
30
+ if is_windows
31
+ extension_dir = "ext,"
32
+ else
33
+ extension_dir = ""
34
+ end
35
+ candidates = Dir.glob("{#{extension_dir}lib,spec}/**/*") +
36
+ ["LICENSE", "README.rdoc", "Rakefile", "right-popen.gemspec"]
37
+ candidates = candidates.delete_if do |item|
38
+ item.include?("Makefile") || item.include?(".obj") || item.include?(".pdb") || item.include?(".def") || item.include?(".exp") || item.include?(".lib")
39
+ end
40
+ candidates = candidates.delete_if do |item|
41
+ if is_windows
42
+ item.include?("/linux/")
43
+ else
44
+ item.include?("/win32/")
45
+ end
46
+ end
47
+ spec.files = candidates.sort!
48
+
49
+ # Current implementation doesn't support > 0.12.8, but support any patched 0.12.8.x versions.
50
+ spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.8", "< 0.12.9"])
51
+ if is_windows
52
+ spec.add_runtime_dependency(%q<win32-process>, [">= 0.6.1"])
53
+ end
54
+ end
55
+
56
+ if $PROGRAM_NAME == __FILE__
57
+ Gem.manage_gems if Gem::RubyGemsVersion.to_f < 1.0
58
+ Gem::Builder.new(spec).build
59
+ end
@@ -0,0 +1,8 @@
1
+ count = ARGV[0] ? ARGV[0].to_i : 1
2
+
3
+ count.times do |i|
4
+ $stderr.puts "stderr #{i}" if 0 == i % 10
5
+ $stdout.puts "stdout #{i}"
6
+ end
7
+
8
+ exit 99
@@ -0,0 +1,2 @@
1
+ $stdout.puts ARGV[0]
2
+ $stderr.puts ARGV[1]
@@ -0,0 +1 @@
1
+ exit ARGV[0].to_i
@@ -0,0 +1,5 @@
1
+ count = ARGV[0] ? ARGV[0].to_i : 1
2
+
3
+ count.times do |i|
4
+ $stderr.puts "stderr #{i}"
5
+ end
@@ -0,0 +1,5 @@
1
+ count = ARGV[0] ? ARGV[0].to_i : 1
2
+
3
+ count.times do |i|
4
+ $stdout.puts "stdout #{i}"
5
+ end
@@ -0,0 +1,143 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'right_popen'
3
+
4
+ RUBY_CMD = 'ruby'
5
+ STANDARD_MESSAGE = 'Standard message'
6
+ ERROR_MESSAGE = 'Error message'
7
+ EXIT_STATUS = 146
8
+ LARGE_OUTPUT_COUNTER = 1000
9
+
10
+ describe 'RightScale::popen3' do
11
+
12
+ def on_read_stdout(data)
13
+ @stdoutput << data
14
+ end
15
+
16
+ def on_read_stderr(data)
17
+ @stderr << data
18
+ end
19
+
20
+ def on_exit(status)
21
+ @done = status
22
+ end
23
+
24
+ before(:each) do
25
+ @done = false
26
+ @stdoutput = ''
27
+ @stderr = ''
28
+ end
29
+
30
+ it 'should redirect output' do
31
+ EM.next_tick do
32
+ cmd = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
33
+ RightScale.popen3(cmd, self, :on_read_stdout, :on_read_stderr, :on_exit)
34
+ end
35
+ EM.run do
36
+ timer = EM::PeriodicTimer.new(0.1) do
37
+ if @done
38
+ timer.cancel
39
+ @stdoutput.should == STANDARD_MESSAGE + "\n"
40
+ @stderr.should == ERROR_MESSAGE + "\n"
41
+ EM.stop
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ it 'should return the right status' do
48
+ EM.next_tick do
49
+ cmd = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_status.rb'))}\" #{EXIT_STATUS}"
50
+ RightScale.popen3(cmd, self, nil, nil, :on_exit)
51
+ end
52
+ EM.run do
53
+ timer = EM::PeriodicTimer.new(0.1) do
54
+ if @done
55
+ timer.cancel
56
+ @done.exitstatus.should == EXIT_STATUS
57
+ EM.stop
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ it 'should preserve the integrity of stdout when stderr is unavailable' do
64
+ # manually bump count up for more aggressive multi-processor testing, lessen
65
+ # for a quick smoke test
66
+ count = LARGE_OUTPUT_COUNTER
67
+ EM.next_tick do
68
+ cmd = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stdout_only.rb'))}\" #{count}"
69
+ RightScale.popen3(cmd, self, :on_read_stdout, :on_read_stderr, :on_exit)
70
+ end
71
+ EM.run do
72
+ timer = EM::PeriodicTimer.new(0.1) do
73
+ if @done
74
+ timer.cancel
75
+ results = []
76
+ count.times do |i|
77
+ results << "stdout #{i}"
78
+ end
79
+ @stdoutput.should == results.join("\n") + "\n"
80
+ results = []
81
+ @stderr.should == ''
82
+ EM.stop
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ it 'should preserve the integrity of stderr when stdout is unavailable' do
89
+ # manually bump count up for more aggressive multi-processor testing, lessen
90
+ # for a quick smoke test
91
+ count = LARGE_OUTPUT_COUNTER
92
+ EM.next_tick do
93
+ cmd = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stderr_only.rb'))}\" #{count}"
94
+ RightScale.popen3(cmd, self, :on_read_stdout, :on_read_stderr, :on_exit)
95
+ end
96
+ EM.run do
97
+ timer = EM::PeriodicTimer.new(0.1) do
98
+ if @done
99
+ timer.cancel
100
+ results = []
101
+ count.times do |i|
102
+ results << "stderr #{i}"
103
+ end
104
+ @stderr.should == results.join("\n") + "\n"
105
+ results = []
106
+ @stdoutput.should == ''
107
+ EM.stop
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ it 'should preserve the integrity of stdout and stderr despite interleaving' do
114
+ # manually bump count up for more aggressive multi-processor testing, lessen
115
+ # for a quick smoke test
116
+ count = LARGE_OUTPUT_COUNTER
117
+ EM.next_tick do
118
+ cmd = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{count}"
119
+ RightScale.popen3(cmd, self, :on_read_stdout, :on_read_stderr, :on_exit)
120
+ end
121
+ EM.run do
122
+ timer = EM::PeriodicTimer.new(0.1) do
123
+ if @done
124
+ timer.cancel
125
+ results = []
126
+ count.times do |i|
127
+ results << "stdout #{i}"
128
+ end
129
+ @stdoutput.should == results.join("\n") + "\n"
130
+ results = []
131
+ count.times do |i|
132
+ (results << "stderr #{i}") if 0 == i % 10
133
+ end
134
+ @stderr.should == results.join("\n") + "\n"
135
+ results = []
136
+ @done.exitstatus.should == 99
137
+ EM.stop
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ end