open4 1.1.0 → 1.2.0

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