open4 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -193,6 +193,44 @@ SAMPLES
193
193
  42
194
194
  /dmsp/reference/ruby-1.8.1//lib/ruby/1.8/timeout.rb:42:in `relay': execution expired (Timeout::Error)
195
195
 
196
+ ----------------------------------------------------------------------------
197
+ pfork4 is similar to popen4, but instead of executing a command, it runs
198
+ ruby code in a child process. if the child process raises an exception, it
199
+ propagates to the parent.
200
+ ----------------------------------------------------------------------------
201
+
202
+ harp: > cat sample/pfork4.rb
203
+ require 'open4'
204
+
205
+ echo = lambda do
206
+ $stdout.write $stdin.read
207
+ raise 'finish implementing me'
208
+ end
209
+
210
+ org_message = "hello, world!"
211
+ got_message = nil
212
+ exception = nil
213
+
214
+ begin
215
+ Open4.pfork4(echo) do |cid, stdin, stdout, stderr|
216
+ stdin.write org_message
217
+ stdin.close
218
+ got_message = stdout.read
219
+ end
220
+ rescue RuntimeError => e
221
+ exception = e.to_s
222
+ end
223
+
224
+ puts "org_message: #{org_message}"
225
+ puts "got_message: #{got_message}"
226
+ puts "exception : #{exception}"
227
+
228
+
229
+ harp: > ruby sample/pfork4.rb
230
+ org_message: hello, world!
231
+ got_message: hello, world!
232
+ exception : finish implementing me
233
+
196
234
  HISTORY
197
235
  1.0.0
198
236
  - added ability for spawn to take a proc (respond_to?(:call))
data/README.erb CHANGED
@@ -193,6 +193,44 @@ SAMPLES
193
193
  42
194
194
  /dmsp/reference/ruby-1.8.1//lib/ruby/1.8/timeout.rb:42:in `relay': execution expired (Timeout::Error)
195
195
 
196
+ ----------------------------------------------------------------------------
197
+ pfork4 is similar to popen4, but instead of executing a command, it runs
198
+ ruby code in a child process. if the child process raises an exception, it
199
+ propagates to the parent.
200
+ ----------------------------------------------------------------------------
201
+
202
+ harp: > cat sample/pfork4.rb
203
+ require 'open4'
204
+
205
+ echo = lambda do
206
+ $stdout.write $stdin.read
207
+ raise 'finish implementing me'
208
+ end
209
+
210
+ org_message = "hello, world!"
211
+ got_message = nil
212
+ exception = nil
213
+
214
+ begin
215
+ Open4.pfork4(echo) do |cid, stdin, stdout, stderr|
216
+ stdin.write org_message
217
+ stdin.close
218
+ got_message = stdout.read
219
+ end
220
+ rescue RuntimeError => e
221
+ exception = e.to_s
222
+ end
223
+
224
+ puts "org_message: #{org_message}"
225
+ puts "got_message: #{got_message}"
226
+ puts "exception : #{exception}"
227
+
228
+
229
+ harp: > ruby sample/pfork4.rb
230
+ org_message: hello, world!
231
+ got_message: hello, world!
232
+ exception : finish implementing me
233
+
196
234
  HISTORY
197
235
  1.0.0
198
236
  - added ability for spawn to take a proc (respond_to?(:call))
@@ -4,19 +4,46 @@ require 'timeout'
4
4
  require 'thread'
5
5
 
6
6
  module Open4
7
- VERSION = '1.1.0'
7
+ VERSION = '1.2.0'
8
8
  def self.version() VERSION end
9
9
 
10
10
  class Error < ::StandardError; end
11
11
 
12
+ def pfork4(fun, &b)
13
+ Open4.do_popen(b, :block) do |ps_read, _|
14
+ ps_read.close
15
+ begin
16
+ fun.call
17
+ rescue SystemExit => e
18
+ # Make it seem to the caller that calling Kernel#exit in +fun+ kills
19
+ # the child process normally. Kernel#exit! bypasses this rescue
20
+ # block.
21
+ exit! e.status
22
+ else
23
+ exit! 0
24
+ end
25
+ end
26
+ end
27
+ module_function :pfork4
28
+
12
29
  def popen4(*cmd, &b)
30
+ Open4.do_popen(b, :init) do |ps_read, ps_write|
31
+ ps_read.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
32
+ ps_write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
33
+ exec(*cmd)
34
+ raise 'forty-two' # Is this really needed?
35
+ end
36
+ end
37
+ alias open4 popen4
38
+ module_function :popen4
39
+ module_function :open4
40
+
41
+ def self.do_popen(b = nil, exception_propagation_at = nil, &cmd)
13
42
  pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
14
43
 
15
44
  verbose = $VERBOSE
16
45
  begin
17
46
  $VERBOSE = nil
18
- ps.first.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
19
- ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
20
47
 
21
48
  cid = fork {
22
49
  pw.last.close
@@ -34,48 +61,57 @@ module Open4
34
61
  STDOUT.sync = STDERR.sync = true
35
62
 
36
63
  begin
37
- exec(*cmd)
38
- raise 'forty-two'
64
+ cmd.call(ps)
39
65
  rescue Exception => e
40
66
  Marshal.dump(e, ps.last)
41
67
  ps.last.flush
68
+ ensure
69
+ ps.last.close unless ps.last.closed?
42
70
  end
43
- ps.last.close unless (ps.last.closed?)
71
+
44
72
  exit!
45
73
  }
46
74
  ensure
47
75
  $VERBOSE = verbose
48
76
  end
49
77
 
50
- [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close}
78
+ [ pw.first, pr.last, pe.last, ps.last ].each { |fd| fd.close }
51
79
 
52
- begin
53
- e = Marshal.load ps.first
54
- raise(Exception === e ? e : "unknown failure!")
55
- rescue EOFError # If we get an EOF error, then the exec was successful
56
- 42
57
- ensure
58
- ps.first.close
59
- end
80
+ Open4.propagate_exception cid, ps.first if exception_propagation_at == :init
60
81
 
61
82
  pw.last.sync = true
62
83
 
63
- pi = [pw.last, pr.first, pe.first]
84
+ pi = [ pw.last, pr.first, pe.first ]
85
+
86
+ begin
87
+ return [cid, *pi] unless b
64
88
 
65
- if b
66
89
  begin
67
- b[cid, *pi]
68
- Process.waitpid2(cid).last
90
+ b.call(cid, *pi)
69
91
  ensure
70
- pi.each{|fd| fd.close unless fd.closed?}
92
+ pi.each { |fd| fd.close unless fd.closed? }
71
93
  end
72
- else
73
- [cid, pw.last, pr.first, pe.first]
94
+
95
+ Open4.propagate_exception cid, ps.first if exception_propagation_at == :block
96
+
97
+ Process.waitpid2(cid).last
98
+ ensure
99
+ ps.first.close unless ps.first.closed?
74
100
  end
75
101
  end
76
- alias open4 popen4
77
- module_function :popen4
78
- module_function :open4
102
+
103
+ def self.propagate_exception(cid, ps_read)
104
+ e = Marshal.load ps_read
105
+ raise Exception === e ? e : "unknown failure!"
106
+ rescue EOFError
107
+ # Child process did not raise exception.
108
+ rescue
109
+ # Child process raised exception; wait it in order to avoid a zombie.
110
+ Process.waitpid2 cid
111
+ raise
112
+ ensure
113
+ ps_read.close
114
+ end
79
115
 
80
116
  class SpawnError < Error
81
117
  attr 'cmd'
@@ -3,7 +3,7 @@
3
3
 
4
4
  Gem::Specification::new do |spec|
5
5
  spec.name = "open4"
6
- spec.version = "1.1.0"
6
+ spec.version = "1.2.0"
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.summary = "open4"
9
9
  spec.description = "description: open4 kicks the ass"
@@ -12,19 +12,25 @@ Gem::Specification::new do |spec|
12
12
  ["LICENSE",
13
13
  "README",
14
14
  "README.erb",
15
- "Rakefile",
16
15
  "lib",
17
16
  "lib/open4.rb",
18
17
  "open4.gemspec",
18
+ "rakefile",
19
19
  "samples",
20
20
  "samples/bg.rb",
21
21
  "samples/block.rb",
22
22
  "samples/exception.rb",
23
23
  "samples/jesse-caldwell.rb",
24
+ "samples/pfork4.rb",
24
25
  "samples/simple.rb",
25
26
  "samples/spawn.rb",
26
27
  "samples/stdin_timeout.rb",
27
28
  "samples/timeout.rb",
29
+ "test",
30
+ "test/pfork4_test.rb",
31
+ "test/popen4_test.rb",
32
+ "test/support",
33
+ "test/support/test_case.rb",
28
34
  "white_box",
29
35
  "white_box/leak.rb"]
30
36
 
@@ -9,19 +9,8 @@ task :default do
9
9
  end
10
10
 
11
11
  task :test do
12
- run_tests!
13
- end
14
-
15
- namespace :test do
16
- task(:unit){ run_tests!(:unit) }
17
- task(:functional){ run_tests!(:functional) }
18
- task(:integration){ run_tests!(:integration) }
19
- end
20
-
21
- def run_tests!(which = nil)
22
- which ||= '**'
23
12
  test_dir = File.join(This.dir, "test")
24
- test_glob ||= File.join(test_dir, "#{ which }/**_test.rb")
13
+ test_glob ||= File.join(test_dir, "/**_test.rb")
25
14
  test_rbs = Dir.glob(test_glob).sort
26
15
 
27
16
  div = ('=' * 119)
@@ -29,7 +18,7 @@ def run_tests!(which = nil)
29
18
 
30
19
  test_rbs.each_with_index do |test_rb, index|
31
20
  testno = index + 1
32
- command = "#{ This.ruby } -I ./lib -I ./test/lib #{ test_rb }"
21
+ command = "#{ This.ruby } -rubygems -I ./lib -I ./test/support #{ test_rb }"
33
22
 
34
23
  puts
35
24
  say(div, :color => :cyan, :bold => true)
@@ -0,0 +1,24 @@
1
+ require 'open4'
2
+
3
+ echo = lambda do
4
+ $stdout.write $stdin.read
5
+ raise 'finish implementing me'
6
+ end
7
+
8
+ org_message = "hello, world!"
9
+ got_message = nil
10
+ exception = nil
11
+
12
+ begin
13
+ Open4.pfork4(echo) do |cid, stdin, stdout, stderr|
14
+ stdin.write org_message
15
+ stdin.close
16
+ got_message = stdout.read
17
+ end
18
+ rescue RuntimeError => e
19
+ exception = e.to_s
20
+ end
21
+
22
+ puts "org_message: #{org_message}"
23
+ puts "got_message: #{got_message}"
24
+ puts "exception : #{exception}"
@@ -0,0 +1,150 @@
1
+ require 'test_case'
2
+
3
+ module Open4
4
+
5
+ class PFork4Test < TestCase
6
+ def test_fun_successful_return
7
+ fun = lambda { 'lucky me' }
8
+ cid, _ = pfork4 fun
9
+ assert_equal 0, wait_status(cid)
10
+ end
11
+
12
+ def test_fun_force_exit
13
+ exit_code = 43
14
+ fun = lambda { exit! exit_code }
15
+ cid, _ = pfork4 fun
16
+ assert_equal exit_code, wait_status(cid)
17
+ end
18
+
19
+ def test_fun_normal_exit
20
+ exit_code = 43
21
+ fun = lambda { exit exit_code }
22
+ cid, _ = pfork4 fun
23
+ assert_equal exit_code, wait_status(cid)
24
+ end
25
+
26
+ def test_fun_does_not_propagate_exception_without_block
27
+ fun = lambda { raise MyError }
28
+ cid, _ = pfork4 fun
29
+ refute_equal 0, wait_status(cid)
30
+ end
31
+
32
+ def test_fun_propagate_exception_with_block
33
+ fun = lambda { raise MyError }
34
+ assert_raises(MyError) { pfork4(fun) {} }
35
+ end
36
+
37
+ def test_fun_propagate_exception_with_block_avoids_zombie_child_process
38
+ fun = lambda { raise MyError }
39
+ assert_raises(MyError) { pfork4(fun) {} }
40
+ assert_empty Process.waitall
41
+ end
42
+
43
+ def test_call_block_upon_exception
44
+ fun = lambda { raise MyError }
45
+ block_called = false
46
+ assert_raises(MyError) { pfork4(fun) { block_called = true } }
47
+ assert_equal true, block_called
48
+ end
49
+
50
+ def test_passes_child_pid_to_block
51
+ fun = lambda { $stdout.write Process.pid }
52
+ cid_in_block = nil
53
+ cid_in_fun = nil
54
+ status = pfork4(fun) do |cid, _, stdout, _|
55
+ cid_in_block = cid
56
+ cid_in_fun = stdout.read.to_i
57
+ end
58
+ assert_equal cid_in_fun, cid_in_block
59
+ end
60
+
61
+ def test_io_pipes_without_block
62
+ via_msg = 'foo'
63
+ err_msg = 'bar'
64
+ fun = lambda do
65
+ $stdout.write $stdin.read
66
+ $stderr.write err_msg
67
+ end
68
+ out_actual, err_actual = nil, nil
69
+ cid, stdin, stdout, stderr = pfork4 fun
70
+ stdin.write via_msg
71
+ stdin.close
72
+ out_actual = stdout.read
73
+ err_actual = stderr.read
74
+ assert_equal via_msg, out_actual
75
+ assert_equal err_msg, err_actual
76
+ assert_equal 0, wait_status(cid)
77
+ end
78
+
79
+ def test_io_pipes_with_block
80
+ via_msg = 'foo'
81
+ err_msg = 'bar'
82
+ fun = lambda do
83
+ $stdout.write $stdin.read
84
+ $stderr.write err_msg
85
+ end
86
+ out_actual, err_actual = nil, nil
87
+ status = pfork4(fun) do |_, stdin, stdout, stderr|
88
+ stdin.write via_msg
89
+ stdin.close
90
+ out_actual = stdout.read
91
+ err_actual = stderr.read
92
+ end
93
+ assert_equal via_msg, out_actual
94
+ assert_equal err_msg, err_actual
95
+ assert_equal 0, status.exitstatus
96
+ end
97
+
98
+ def test_exec_in_fun
99
+ via_msg = 'foo'
100
+ fun = lambda { exec %{ruby -e "print '#{via_msg}'"} }
101
+ out_actual = nil
102
+ status = pfork4(fun) do |_, stdin, stdout, _|
103
+ stdin.close
104
+ out_actual = stdout.read
105
+ end
106
+ assert_equal via_msg, out_actual
107
+ assert_equal 0, status.exitstatus
108
+ end
109
+
110
+ def test_io_pipes_and_then_exception_propagation_with_block
111
+ via_msg = 'foo'
112
+ err_msg = 'bar'
113
+ fun = lambda do
114
+ $stdout.write $stdin.read
115
+ $stderr.write err_msg
116
+ raise MyError
117
+ end
118
+ out_actual, err_actual = nil, nil
119
+ assert_raises(MyError) do
120
+ pfork4(fun) do |_, stdin, stdout, stderr|
121
+ stdin.write via_msg
122
+ stdin.close
123
+ out_actual = stdout.read
124
+ err_actual = stderr.read
125
+ end
126
+ end
127
+ assert_equal via_msg, out_actual
128
+ assert_equal err_msg, err_actual
129
+ end
130
+
131
+ def test_blocked_on_io_read_and_exception_propagation_with_block
132
+ fun = lambda do
133
+ $stdin.read
134
+ raise MyError
135
+ end
136
+ out_actual, err_actual = nil, nil
137
+ assert_raises(MyError) do
138
+ pfork4(fun) do |_, stdin, stdout, stderr|
139
+ stdin.write 'foo'
140
+ stdin.close
141
+ out_actual = stdout.read
142
+ err_actual = stderr.read
143
+ end
144
+ end
145
+ assert_equal '', out_actual
146
+ assert_equal '', err_actual
147
+ end
148
+ end
149
+
150
+ end
@@ -0,0 +1,82 @@
1
+ require 'test_case'
2
+
3
+ module Open4
4
+
5
+ class POpen4Test < TestCase
6
+ UNKNOWN_CMD = 'asdfadsfjlkkk'
7
+ UNKNOWN_CMD_ERRORS = [Errno::ENOENT, Errno::EINVAL]
8
+
9
+ def test_unknown_command_propagates_exception
10
+ err = assert_raises(*UNKNOWN_CMD_ERRORS) { popen4 UNKNOWN_CMD }
11
+ assert_match /#{UNKNOWN_CMD}/, err.to_s if on_mri?
12
+ end
13
+
14
+ def test_exception_propagation_avoids_zombie_child_process
15
+ assert_raises(*UNKNOWN_CMD_ERRORS) { popen4 UNKNOWN_CMD }
16
+ assert_empty Process.waitall
17
+ end
18
+
19
+ def test_exit_failure
20
+ code = 43
21
+ cid, _ = popen4 %{ruby -e "exit #{43}"}
22
+ assert_equal code, wait_status(cid)
23
+ end
24
+
25
+ def test_exit_success
26
+ cid, _ = popen4 %{ruby -e "exit"}
27
+ assert_equal 0, wait_status(cid)
28
+ end
29
+
30
+ def test_passes_child_pid_to_block
31
+ cmd = %{ruby -e "STDOUT.print Process.pid"}
32
+ cid_in_block = nil
33
+ cid_in_fun = nil
34
+ status = popen4(cmd) do |cid, _, stdout, _|
35
+ cid_in_block = cid
36
+ cid_in_fun = stdout.read.to_i
37
+ end
38
+ assert_equal cid_in_fun, cid_in_block
39
+ end
40
+
41
+ def test_io_pipes_without_block
42
+ via_msg = 'foo'
43
+ err_msg = 'bar'
44
+ cmd = <<-END
45
+ ruby -e "
46
+ STDOUT.write STDIN.read
47
+ STDERR.write '#{err_msg}'
48
+ "
49
+ END
50
+ cid, stdin, stdout, stderr = popen4 cmd
51
+ stdin.write via_msg
52
+ stdin.close
53
+ out_actual = stdout.read
54
+ err_actual = stderr.read
55
+ assert_equal via_msg, out_actual
56
+ assert_equal err_msg, err_actual
57
+ assert_equal 0, wait_status(cid)
58
+ end
59
+
60
+ def test_io_pipes_with_block
61
+ via_msg = 'foo'
62
+ err_msg = 'bar'
63
+ out_actual, err_actual = nil
64
+ cmd = <<-END
65
+ ruby -e "
66
+ STDOUT.write STDIN.read
67
+ STDERR.write '#{err_msg}'
68
+ "
69
+ END
70
+ status = popen4(cmd) do |_, stdin, stdout, stderr|
71
+ stdin.write via_msg
72
+ stdin.close
73
+ out_actual = stdout.read
74
+ err_actual = stderr.read
75
+ end
76
+ assert_equal via_msg, out_actual
77
+ assert_equal err_msg, err_actual
78
+ assert_equal 0, status.exitstatus
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+
3
+ require 'minitest/autorun'
4
+ require 'open4'
5
+ require 'rbconfig'
6
+
7
+ module Open4
8
+ class TestCase < MiniTest::Unit::TestCase
9
+ include Open4
10
+
11
+ # Custom exception class for tests so we don't shadow possible
12
+ # programming errors.
13
+ class MyError < RuntimeError; end
14
+
15
+ def on_mri?
16
+ ::Config::CONFIG['ruby_install_name'] == 'ruby'
17
+ end
18
+
19
+ def wait_status(cid)
20
+ Process.waitpid2(cid).last.exitstatus
21
+ end
22
+ end
23
+ end
metadata CHANGED
@@ -1,8 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open4
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 31
4
5
  prerelease:
5
- version: 1.1.0
6
+ segments:
7
+ - 1
8
+ - 2
9
+ - 0
10
+ version: 1.2.0
6
11
  platform: ruby
7
12
  authors:
8
13
  - Ara T. Howard
@@ -10,7 +15,7 @@ autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
17
 
13
- date: 2011-06-24 00:00:00 Z
18
+ date: 2011-10-05 00:00:00 Z
14
19
  dependencies: []
15
20
 
16
21
  description: "description: open4 kicks the ass"
@@ -25,21 +30,27 @@ files:
25
30
  - LICENSE
26
31
  - README
27
32
  - README.erb
28
- - Rakefile
29
33
  - lib/open4.rb
30
34
  - open4.gemspec
35
+ - rakefile
31
36
  - samples/bg.rb
32
37
  - samples/block.rb
33
38
  - samples/exception.rb
34
39
  - samples/jesse-caldwell.rb
40
+ - samples/pfork4.rb
35
41
  - samples/simple.rb
36
42
  - samples/spawn.rb
37
43
  - samples/stdin_timeout.rb
38
44
  - samples/timeout.rb
45
+ - test/pfork4_test.rb
46
+ - test/popen4_test.rb
47
+ - test/support/test_case.rb
39
48
  - white_box/leak.rb
40
49
  homepage: https://github.com/ahoward/open4
41
50
  licenses: []
42
51
 
52
+ metadata: {}
53
+
43
54
  post_install_message:
44
55
  rdoc_options: []
45
56
 
@@ -50,19 +61,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
61
  requirements:
51
62
  - - ">="
52
63
  - !ruby/object:Gem::Version
64
+ hash: 3
65
+ segments:
66
+ - 0
53
67
  version: "0"
54
68
  required_rubygems_version: !ruby/object:Gem::Requirement
55
69
  none: false
56
70
  requirements:
57
71
  - - ">="
58
72
  - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
59
76
  version: "0"
60
77
  requirements: []
61
78
 
62
79
  rubyforge_project: codeforpeople
63
- rubygems_version: 1.7.2
80
+ rubygems_version: 1.8.10
64
81
  signing_key:
65
- specification_version: 3
82
+ specification_version: 4
66
83
  summary: open4
67
84
  test_files: []
68
85