├── lib ├── posix-spawn.rb └── posix │ ├── spawn │ ├── version.rb │ └── child.rb │ └── spawn.rb ├── Gemfile ├── .gitignore ├── test ├── test_helper.rb ├── test_popen.rb ├── test_system.rb ├── test_backtick.rb ├── test_child.rb └── test_spawn.rb ├── ext ├── extconf.rb └── posix-spawn.c ├── HACKING ├── posix-spawn.gemspec ├── TODO ├── COPYING ├── Rakefile ├── bin └── posix-spawn-benchmark └── README.md /lib/posix-spawn.rb: -------------------------------------------------------------------------------- 1 | require "posix/spawn" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | ext/Makefile 3 | lib/posix_spawn_ext.* 4 | tmp 5 | pkg 6 | .bundle 7 | -------------------------------------------------------------------------------- /lib/posix/spawn/version.rb: -------------------------------------------------------------------------------- 1 | module POSIX 2 | module Spawn 3 | VERSION = '0.3.11' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'posix-spawn' 3 | 4 | if Minitest.const_defined?('Test') 5 | # We're on Minitest 5+. Nothing to do here. 6 | else 7 | # Minitest 4 doesn't have Minitest::Test yet. 8 | Minitest::Test = MiniTest::Unit::TestCase 9 | end 10 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | # warnings save lives 4 | $CFLAGS << " -Wall " if RbConfig::CONFIG['GCC'] != "" 5 | 6 | if RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ 7 | File.open('Makefile','w'){|f| f.puts "default: \ninstall: " } 8 | else 9 | create_makefile('posix_spawn_ext') 10 | end 11 | 12 | -------------------------------------------------------------------------------- /test/test_popen.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PopenTest < Minitest::Test 4 | include POSIX::Spawn 5 | 6 | def test_popen4 7 | pid, i, o, e = popen4("cat") 8 | i.write "hello world" 9 | i.close 10 | ::Process.wait(pid) 11 | 12 | assert_equal "hello world", o.read 13 | assert_equal 0, $?.exitstatus 14 | ensure 15 | [i, o, e].each{ |io| io.close rescue nil } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_system.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SystemTest < Minitest::Test 4 | include POSIX::Spawn 5 | 6 | def test_system 7 | ret = system("true") 8 | assert_equal true, ret 9 | assert_equal 0, $?.exitstatus 10 | end 11 | 12 | def test_system_nonzero 13 | ret = system("false") 14 | assert_equal false, ret 15 | assert_equal 1, $?.exitstatus 16 | end 17 | 18 | def test_system_nonzero_via_sh 19 | ret = system("exit 1") 20 | assert_equal false, ret 21 | assert_equal 1, $?.exitstatus 22 | end 23 | 24 | def test_system_failure 25 | ret = system("nosuch") 26 | assert_equal false, ret 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Clone the project: 2 | 3 | git clone http://github.com/rtomayko/posix-spawn.git 4 | cd posix-spawn 5 | bundle install 6 | 7 | Rake tasks can be run without further setup: 8 | 9 | rake build 10 | rake test 11 | rake benchmark 12 | 13 | Just `rake' builds the extension and runs the tests. 14 | 15 | If you want to run the benchmark scripts or tests directly out of a 16 | working copy, first setup your PATH and RUBYLIB environment: 17 | 18 | PATH="$(pwd)/bin:$PATH" 19 | RUBYLIB="$(pwd)/lib:$(pwd)/ext:$RUBYLIB" 20 | export RUBYLIB 21 | 22 | Or, use the following rbdev script to quickly setup your PATH and 23 | RUBYLIB environment for this and other projects adhering to the 24 | Ruby Packaging Standard: 25 | 26 | https://github.com/rtomayko/dotfiles/blob/rtomayko/bin/rbdev 27 | -------------------------------------------------------------------------------- /posix-spawn.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/posix/spawn/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'posix-spawn' 5 | s.version = POSIX::Spawn::VERSION 6 | 7 | s.summary = 'posix_spawnp(2) for ruby' 8 | s.description = 'posix-spawn uses posix_spawnp(2) for faster process spawning' 9 | 10 | s.homepage = 'https://github.com/rtomayko/posix-spawn' 11 | 12 | s.authors = ['Ryan Tomayko', 'Aman Gupta'] 13 | s.email = ['r@tomayko.com', 'aman@tmm1.net'] 14 | s.licenses = ['MIT'] 15 | 16 | s.add_development_dependency 'rake-compiler', '0.7.6' 17 | s.add_development_dependency 'minitest', '>= 4' 18 | 19 | s.extensions = ['ext/extconf.rb'] 20 | s.executables << 'posix-spawn-benchmark' 21 | s.require_paths = ['lib'] 22 | 23 | s.files = `git ls-files`.split("\n") 24 | s.extra_rdoc_files = %w[ COPYING HACKING ] 25 | end 26 | -------------------------------------------------------------------------------- /test/test_backtick.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BacktickTest < Minitest::Test 4 | include POSIX::Spawn 5 | 6 | def test_backtick_simple 7 | out = `exit` 8 | assert_equal '', out 9 | assert_equal 0, $?.exitstatus 10 | end 11 | 12 | def test_backtick_output 13 | out = `echo 123` 14 | assert_equal "123\n", out 15 | assert_equal 0, $?.exitstatus, 0 16 | end 17 | 18 | def test_backtick_failure 19 | out = `nosuchcmd 2> /dev/null` 20 | assert_equal '', out 21 | assert_equal 127, $?.exitstatus 22 | end 23 | 24 | def test_backtick_redirect 25 | out = `nosuchcmd 2>&1` 26 | regex = %r{/bin/sh: (1: )?nosuchcmd: (command )?not found} 27 | assert regex.match(out), "Got #{out.inspect}, expected match of pattern #{regex.inspect}" 28 | assert_equal 127, $?.exitstatus, 127 29 | end 30 | 31 | def test_backtick_huge 32 | out = `yes | head -50000` 33 | assert_equal 100000, out.size 34 | assert_equal 0, $?.exitstatus 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | [x] license (LGPL) 2 | [x] fucking name this thing 3 | [x] fastspawn-bm should take iterations and memsize arguments 4 | [x] high level Grit::Process like class (string based) (tmm1) 5 | [x] add FD => '/path/to/file' (all variations) (rtomayko) 6 | [x] raise exception on unhandled options 7 | [x] benchmarks in README (tmm1) 8 | [x] POSIX::Spawn::spawn usage examples in README 9 | [x] POSIX::Spawn#pspawn should be just #spawn 10 | [x] :err => :out case -- currently closing out after dup2'ing 11 | [x] POSIX::Spawn::Process.new should have same method signature as Process::spawn 12 | [x] POSIX::Spawn::Process renamed to POSIX::Spawn::Child 13 | [x] Better POSIX::Spawn#spawn comment docs 14 | [x] POSIX::Spawn::Child usage examples in README 15 | 16 | 17 | [ ] popen* interfaces 18 | [x] system interface 19 | [x] ` interface 20 | [ ] jruby Grit::Process stuff 21 | [ ] make :vfork an option to Spawn#spawn 22 | [ ] check all posix_spawn_* function call return values 23 | [ ] POSIX::Spawn as ::Spawn? (maybe, we'll see) 24 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Ryan Tomayko 2 | and Aman Gupta 3 | 4 | Permission is hereby granted, free of charge, to any person ob- 5 | taining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restric- 7 | tion, including without limitation the rights to use, copy, modi- 8 | fy, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is fur- 10 | nished to do so, subject to 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 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN- 18 | FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 20 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :test 2 | 3 | # ========================================================== 4 | # Packaging 5 | # ========================================================== 6 | 7 | GEMSPEC = eval(File.read('posix-spawn.gemspec')) 8 | 9 | require 'rubygems/package_task' 10 | Gem::PackageTask.new(GEMSPEC) do |pkg| 11 | end 12 | 13 | # ========================================================== 14 | # Ruby Extension 15 | # ========================================================== 16 | 17 | begin 18 | require 'rake/extensiontask' 19 | rescue LoadError => boom 20 | warn "ERROR: The rake-compiler gem dependency is missing." 21 | warn "Please run `bundle install' and try again." 22 | raise 23 | end 24 | Rake::ExtensionTask.new('posix_spawn_ext', GEMSPEC) do |ext| 25 | ext.ext_dir = 'ext' 26 | end 27 | task :build => :compile 28 | 29 | # ========================================================== 30 | # Testing 31 | # ========================================================== 32 | 33 | require 'rake/testtask' 34 | Rake::TestTask.new 'test' do |t| 35 | t.libs << "test" 36 | t.test_files = FileList['test/test_*.rb'] 37 | end 38 | task :test => :build 39 | 40 | desc 'Run some benchmarks' 41 | task :benchmark => :build do 42 | ruby '-Ilib', 'bin/posix-spawn-benchmark' 43 | end 44 | -------------------------------------------------------------------------------- /bin/posix-spawn-benchmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #/ Usage: posix-spawn-benchmark [-n ] [-m ] 3 | #/ Run posix-spawn (Ruby extension) benchmarks and report to standard output. 4 | #/ 5 | #/ Options: 6 | #/ -n, --count=NUM total number of processes to spawn. 7 | #/ -m, --mem-size=MB RES size to bloat to before performing benchmarks. 8 | #/ -g, --graph benchmark at 10MB itervals up to RES and graph results. 9 | #/ 10 | #/ Benchmarks run with -n 1000 -m 100 by default. 11 | require 'optparse' 12 | require 'posix-spawn' 13 | require 'benchmark' 14 | include Benchmark 15 | 16 | allocate = 100 * (1024 ** 2) 17 | iterations = 1_000 18 | graphmode = false 19 | ARGV.options do |o| 20 | o.set_summary_indent(' ') 21 | o.on("-n", "--count=num") { |val| iterations = val.to_i } 22 | o.on("-m", "--mem-size=MB") { |val| allocate = val.to_i * (1024 ** 2) } 23 | o.on("-g", "--graph") { graphmode = true } 24 | o.on_tail("-h", "--help") { exec "grep ^#/ <'#{__FILE__}' |cut -c4-" } 25 | o.parse! 26 | end 27 | 28 | if graphmode 29 | bloat = [] 30 | data = {} 31 | chunk = allocate / 10 32 | max = 0 33 | 34 | 10.times do 35 | puts "allocating #{chunk / (1024 ** 2)}MB (#{(bloat.size+1) * chunk / (1024 ** 2)}MB total)" 36 | bloat << ('x' * chunk) 37 | # size = bloat.size / (1024 ** 2) 38 | 39 | %w[ fspawn pspawn ].each do |type| 40 | print " - benchmarking #{type}... " 41 | time = Benchmark.realtime do 42 | iterations.times do 43 | pid = POSIX::Spawn.send(type, 'true') 44 | Process.wait(pid) 45 | end 46 | end 47 | puts "done (#{time})" 48 | 49 | data[type] ||= [] 50 | data[type] << time 51 | max = time if time > max 52 | end 53 | end 54 | 55 | max = max < 0.5 ? (max * 10).round / 10.0 : max.ceil 56 | minmb, maxmb = chunk/(1024**2), allocate/(1024**2) 57 | series = %w[ fspawn pspawn ].map{|name| data[name].map{|d| "%.2f" % d }.join(',') } 58 | 59 | chart = { 60 | :chs => '900x200', 61 | :cht => 'bvg', # grouped vertical bar chart 62 | :chtt => "posix-spawn-benchmark --graph --count #{iterations} --mem-size #{maxmb} (#{RUBY_PLATFORM})", 63 | 64 | :chf => 'bg,s,f8f8f8', # background 65 | :chbh => 'a,5,25', # 25px between bar groups 66 | 67 | :chd => "t:#{series.join('|')}", # data 68 | :chds => "0,#{max}", # scale 69 | :chdl => 'fspawn (fork+exec)|pspawn (posix_spawn)', # legend 70 | :chco => '1f77b4,ff7f0e', # colors 71 | 72 | :chxt => 'x,y', 73 | :chxr => "1,0,#{max},#{max/5}", # y labels up to max time 74 | :chxs => '1N** secs', # y labels are +=' secs' 75 | :chxl => "0:|#{minmb.step(maxmb, maxmb/10).map{ |mb| "#{mb} MB"}.join('|')}", # x bucket labels 76 | } 77 | 78 | url = "https://chart.googleapis.com/chart?" 79 | url += chart.map do |key, val| 80 | "#{key}=#{val.gsub(' ','%20').gsub('(','%28').gsub(')','%29').gsub('+','%2B')}" 81 | end.join('&') 82 | url += '#.png' 83 | 84 | puts url 85 | 86 | exit! 87 | end 88 | 89 | puts "benchmarking fork/exec vs. posix_spawn over #{iterations} runs" + 90 | " at #{allocate / (1024 ** 2)}M res" 91 | 92 | # bloat the process 93 | bloat = 'x' * allocate 94 | 95 | # run the benchmarks 96 | bm 40 do |x| 97 | x.report("fspawn (fork/exec):") do 98 | iterations.times do 99 | pid = POSIX::Spawn.fspawn('true') 100 | Process.wait(pid) 101 | end 102 | end 103 | x.report("pspawn (posix_spawn):") do 104 | iterations.times do 105 | pid = POSIX::Spawn.pspawn('true') 106 | Process.wait(pid) 107 | end 108 | end 109 | if Process.respond_to?(:spawn) 110 | x.report("spawn (native):") do 111 | iterations.times do 112 | pid = Process.spawn('true') 113 | Process.wait(pid) 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/test_child.rb: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | require 'test_helper' 3 | 4 | class ChildTest < Minitest::Test 5 | include POSIX::Spawn 6 | 7 | # Become a new process group. 8 | def setup 9 | Process.setpgrp 10 | end 11 | 12 | # Kill any orphaned processes in our process group before continuing but 13 | # ignore the TERM signal we receive. 14 | def teardown 15 | trap("TERM") { trap("TERM", "DEFAULT") } 16 | begin 17 | Process.kill("-TERM", Process.pid) 18 | Process.wait 19 | rescue Errno::ECHILD 20 | end 21 | end 22 | 23 | # verify the process is no longer running and has been reaped. 24 | def assert_process_reaped(pid) 25 | Process.kill(0, pid) 26 | assert false, "Process #{pid} still running" 27 | rescue Errno::ESRCH 28 | end 29 | 30 | # verifies that all processes in the given process group are no longer running 31 | # and have been reaped. The current ruby test process is excluded. 32 | # XXX It's weird to use the SUT here but the :pgroup option is useful. Could 33 | # be a IO.popen under Ruby >= 1.9 since it also supports :pgroup. 34 | def assert_process_group_reaped(pgid) 35 | command = "ps axo pgid,pid,args | grep '^#{pgid} ' | grep -v '^#{pgid} #$$'" 36 | procs = POSIX::Spawn::Child.new(command, :pgroup => true).out 37 | assert procs.empty?, "Processes in group #{pgid} still running:\n#{procs}" 38 | end 39 | 40 | def test_sanity 41 | assert_same POSIX::Spawn::Child, Child 42 | end 43 | 44 | def test_argv_array_execs 45 | p = Child.new('printf', '%s %s %s', '1', '2', '3 4') 46 | assert p.success? 47 | assert_equal "1 2 3 4", p.out 48 | end 49 | 50 | def test_argv_string_uses_sh 51 | p = Child.new("echo via /bin/sh") 52 | assert p.success? 53 | assert_equal "via /bin/sh\n", p.out 54 | end 55 | 56 | def test_stdout 57 | p = Child.new('echo', 'boom') 58 | assert_equal "boom\n", p.out 59 | assert_equal "", p.err 60 | end 61 | 62 | def test_stderr 63 | p = Child.new('echo boom 1>&2') 64 | assert_equal "", p.out 65 | assert_equal "boom\n", p.err 66 | end 67 | 68 | def test_status 69 | p = Child.new('exit 3') 70 | assert !p.status.success? 71 | assert_equal 3, p.status.exitstatus 72 | end 73 | 74 | def test_env 75 | p = Child.new({ 'FOO' => 'BOOYAH' }, 'echo $FOO') 76 | assert_equal "BOOYAH\n", p.out 77 | end 78 | 79 | def test_chdir 80 | p = Child.new("pwd", :chdir => File.dirname(Dir.pwd)) 81 | assert_equal File.dirname(Dir.pwd) + "\n", p.out 82 | end 83 | 84 | def test_input 85 | input = "HEY NOW\n" * 100_000 # 800K 86 | p = Child.new('wc', '-l', :input => input) 87 | assert_equal 100_000, p.out.strip.to_i 88 | end 89 | 90 | def test_max 91 | child = Child.build('yes', :max => 100_000) 92 | assert_raises(MaximumOutputExceeded) { child.exec! } 93 | assert_process_reaped child.pid 94 | assert_process_group_reaped Process.pid 95 | end 96 | 97 | def test_max_pgroup_kill 98 | child = Child.build('yes', :max => 100_000, :pgroup_kill => true) 99 | assert_raises(MaximumOutputExceeded) { child.exec! } 100 | assert_process_reaped child.pid 101 | assert_process_group_reaped child.pid 102 | end 103 | 104 | def test_max_with_child_hierarchy 105 | child = Child.build('/bin/sh', '-c', 'true && yes', :max => 100_000) 106 | assert_raises(MaximumOutputExceeded) { child.exec! } 107 | assert_process_reaped child.pid 108 | assert_process_group_reaped Process.pid 109 | end 110 | 111 | def test_max_with_child_hierarchy_pgroup_kill 112 | child = Child.build('/bin/sh', '-c', 'true && yes', :max => 100_000, :pgroup_kill => true) 113 | assert_raises(MaximumOutputExceeded) { child.exec! } 114 | assert_process_reaped child.pid 115 | assert_process_group_reaped child.pid 116 | end 117 | 118 | def test_max_with_stubborn_child 119 | child = Child.build("trap '' TERM; yes", :max => 100_000) 120 | assert_raises(MaximumOutputExceeded) { child.exec! } 121 | assert_process_reaped child.pid 122 | assert_process_group_reaped Process.pid 123 | end 124 | 125 | def test_max_with_stubborn_child_pgroup_kill 126 | child = Child.build("trap '' TERM; yes", :max => 100_000, :pgroup_kill => true) 127 | assert_raises(MaximumOutputExceeded) { child.exec! } 128 | assert_process_reaped child.pid 129 | assert_process_group_reaped child.pid 130 | end 131 | 132 | def test_max_with_partial_output 133 | p = Child.build('yes', :max => 100_000) 134 | assert_nil p.out 135 | assert_raises MaximumOutputExceeded do 136 | p.exec! 137 | end 138 | assert_output_exceeds_repeated_string("y\n", 100_000, p.out) 139 | assert_process_reaped p.pid 140 | assert_process_group_reaped Process.pid 141 | end 142 | 143 | def test_max_with_partial_output_long_lines 144 | p = Child.build('yes', "nice to meet you", :max => 10_000) 145 | assert_raises MaximumOutputExceeded do 146 | p.exec! 147 | end 148 | assert_output_exceeds_repeated_string("nice to meet you\n", 10_000, p.out) 149 | assert_process_reaped p.pid 150 | assert_process_group_reaped Process.pid 151 | end 152 | 153 | def test_timeout 154 | start = Time.now 155 | child = Child.build('sleep', '1', :timeout => 0.05) 156 | assert_raises(TimeoutExceeded) { child.exec! } 157 | assert_process_reaped child.pid 158 | assert_process_group_reaped Process.pid 159 | assert (Time.now-start) <= 0.2 160 | end 161 | 162 | def test_timeout_pgroup_kill 163 | start = Time.now 164 | child = Child.build('sleep', '1', :timeout => 0.05, :pgroup_kill => true) 165 | assert_raises(TimeoutExceeded) { child.exec! } 166 | assert_process_reaped child.pid 167 | assert_process_group_reaped child.pid 168 | assert (Time.now-start) <= 0.2 169 | end 170 | 171 | def test_timeout_with_child_hierarchy 172 | child = Child.build('/bin/sh', '-c', 'true && sleep 1', :timeout => 0.05) 173 | assert_raises(TimeoutExceeded) { child.exec! } 174 | assert_process_reaped child.pid 175 | end 176 | 177 | def test_timeout_with_child_hierarchy_pgroup_kill 178 | child = Child.build('/bin/sh', '-c', 'true && sleep 1', :timeout => 0.05, :pgroup_kill => true) 179 | assert_raises(TimeoutExceeded) { child.exec! } 180 | assert_process_reaped child.pid 181 | assert_process_group_reaped child.pid 182 | end 183 | 184 | def test_timeout_with_partial_output 185 | start = Time.now 186 | p = Child.build('echo Hello; sleep 1', :timeout => 0.05, :pgroup_kill => true) 187 | assert_raises(TimeoutExceeded) { p.exec! } 188 | assert_process_reaped p.pid 189 | assert_process_group_reaped Process.pid 190 | assert (Time.now-start) <= 0.2 191 | assert_equal "Hello\n", p.out 192 | end 193 | 194 | def test_lots_of_input_and_lots_of_output_at_the_same_time 195 | input = "stuff on stdin \n" * 1_000 196 | command = " 197 | while read line 198 | do 199 | echo stuff on stdout; 200 | echo stuff on stderr 1>&2; 201 | done 202 | " 203 | p = Child.new(command, :input => input) 204 | assert_equal input.size, p.out.size 205 | assert_equal input.size, p.err.size 206 | assert p.success? 207 | end 208 | 209 | def test_input_cannot_be_written_due_to_broken_pipe 210 | input = "1" * 100_000 211 | p = Child.new('false', :input => input) 212 | assert !p.success? 213 | end 214 | 215 | def test_utf8_input 216 | input = "hålø" 217 | p = Child.new('cat', :input => input) 218 | assert p.success? 219 | end 220 | 221 | def test_utf8_input_long 222 | input = "hålø" * 10_000 223 | p = Child.new('cat', :input => input) 224 | assert p.success? 225 | end 226 | 227 | ## 228 | # Assertion Helpers 229 | 230 | def assert_output_exceeds_repeated_string(str, len, actual) 231 | assert_operator actual.length, :>=, len 232 | 233 | expected = (str * (len / str.length + 1)).slice(0, len) 234 | assert_equal expected, actual.slice(0, len) 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/posix/spawn/child.rb: -------------------------------------------------------------------------------- 1 | require 'posix/spawn' 2 | 3 | module POSIX 4 | module Spawn 5 | # POSIX::Spawn::Child includes logic for executing child processes and 6 | # reading/writing from their standard input, output, and error streams. It's 7 | # designed to take all input in a single string and provides all output 8 | # (stderr and stdout) as single strings and is therefore not well-suited 9 | # to streaming large quantities of data in and out of commands. 10 | # 11 | # Create and run a process to completion: 12 | # 13 | # >> child = POSIX::Spawn::Child.new('git', '--help') 14 | # 15 | # Retrieve stdout or stderr output: 16 | # 17 | # >> child.out 18 | # => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..." 19 | # >> child.err 20 | # => "" 21 | # 22 | # Check process exit status information: 23 | # 24 | # >> child.status 25 | # => # 26 | # 27 | # To write data on the new process's stdin immediately after spawning: 28 | # 29 | # >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2') 30 | # >> child.out 31 | # "42\n" 32 | # 33 | # To access output from the process even if an exception was raised: 34 | # 35 | # >> child = POSIX::Spawn::Child.build('git', 'log', :max => 1000) 36 | # >> begin 37 | # ?> child.exec! 38 | # ?> rescue POSIX::Spawn::MaximumOutputExceeded 39 | # ?> # just so you know 40 | # ?> end 41 | # >> child.out 42 | # "... first 1000 characters of log output ..." 43 | # 44 | # Q: Why use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec 45 | # code, or Process::spawn? 46 | # 47 | # - It's more efficient than popen3 and provides meaningful process 48 | # hierarchies because it performs a single fork/exec. (popen3 double forks 49 | # to avoid needing to collect the exit status and also calls 50 | # Process::detach which creates a Ruby Thread!!!!). 51 | # 52 | # - It handles all max pipe buffer (PIPE_BUF) hang cases when reading and 53 | # writing semi-large amounts of data. This is non-trivial to implement 54 | # correctly and must be accounted for with popen3, spawn, or hand rolled 55 | # fork/exec code. 56 | # 57 | # - It's more portable than hand rolled pipe, fork, exec code because 58 | # fork(2) and exec aren't available on all platforms. In those cases, 59 | # POSIX::Spawn::Child falls back to using whatever janky substitutes 60 | # the platform provides. 61 | class Child 62 | include POSIX::Spawn 63 | 64 | # Spawn a new process, write all input and read all output, and wait for 65 | # the program to exit. Supports the standard spawn interface as described 66 | # in the POSIX::Spawn module documentation: 67 | # 68 | # new([env], command, [argv1, ...], [options]) 69 | # 70 | # The following options are supported in addition to the standard 71 | # POSIX::Spawn options: 72 | # 73 | # :input => str Write str to the new process's standard input. 74 | # :timeout => int Maximum number of seconds to allow the process 75 | # to execute before aborting with a TimeoutExceeded 76 | # exception. 77 | # :max => total Maximum number of bytes of output to allow the 78 | # process to generate before aborting with a 79 | # MaximumOutputExceeded exception. 80 | # :pgroup_kill => bool Boolean specifying whether to kill the process 81 | # group (true) or individual process (false, default). 82 | # Setting this option true implies :pgroup => true. 83 | # 84 | # Returns a new Child instance whose underlying process has already 85 | # executed to completion. The out, err, and status attributes are 86 | # immediately available. 87 | def initialize(*args) 88 | @env, @argv, options = extract_process_spawn_arguments(*args) 89 | @options = options.dup 90 | @input = @options.delete(:input) 91 | @timeout = @options.delete(:timeout) 92 | @max = @options.delete(:max) 93 | if @options.delete(:pgroup_kill) 94 | @pgroup_kill = true 95 | @options[:pgroup] = true 96 | end 97 | @options.delete(:chdir) if @options[:chdir].nil? 98 | exec! if !@options.delete(:noexec) 99 | end 100 | 101 | # Set up a new process to spawn, but do not actually spawn it. 102 | # 103 | # Invoke this just like the normal constructor to set up a process 104 | # to be run. Call `exec!` to actually run the child process, send 105 | # the input, read the output, and wait for completion. Use this 106 | # alternative way of constructing a POSIX::Spawn::Child if you want 107 | # to read any partial output from the child process even after an 108 | # exception. 109 | # 110 | # child = POSIX::Spawn::Child.build(... arguments ...) 111 | # child.exec! 112 | # 113 | # The arguments are the same as the regular constructor. 114 | # 115 | # Returns a new Child instance but does not run the underlying process. 116 | def self.build(*args) 117 | options = 118 | if args[-1].respond_to?(:to_hash) 119 | args.pop.to_hash 120 | else 121 | {} 122 | end 123 | new(*(args + [{ :noexec => true }.merge(options)])) 124 | end 125 | 126 | # All data written to the child process's stdout stream as a String. 127 | attr_reader :out 128 | 129 | # All data written to the child process's stderr stream as a String. 130 | attr_reader :err 131 | 132 | # A Process::Status object with information on how the child exited. 133 | attr_reader :status 134 | 135 | # Total command execution time (wall-clock time) 136 | attr_reader :runtime 137 | 138 | # The pid of the spawned child process. This is unlikely to be a valid 139 | # current pid since Child#exec! doesn't return until the process finishes 140 | # and is reaped. 141 | attr_reader :pid 142 | 143 | # Determine if the process did exit with a zero exit status. 144 | def success? 145 | @status && @status.success? 146 | end 147 | 148 | # Execute command, write input, and read output. This is called 149 | # immediately when a new instance of this object is created, or 150 | # can be called explicitly when creating the Child via `build`. 151 | def exec! 152 | # spawn the process and hook up the pipes 153 | pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options])) 154 | @pid = pid 155 | 156 | # async read from all streams into buffers 157 | read_and_write(@input, stdin, stdout, stderr, @timeout, @max) 158 | 159 | # grab exit status 160 | @status = waitpid(pid) 161 | rescue Object 162 | [stdin, stdout, stderr].each { |fd| fd.close rescue nil } 163 | if @status.nil? 164 | if !@pgroup_kill 165 | ::Process.kill('TERM', pid) rescue nil 166 | else 167 | ::Process.kill('-TERM', pid) rescue nil 168 | end 169 | @status = waitpid(pid) rescue nil 170 | end 171 | raise 172 | ensure 173 | # let's be absolutely certain these are closed 174 | [stdin, stdout, stderr].each { |fd| fd.close rescue nil } 175 | end 176 | 177 | private 178 | # Maximum buffer size for reading 179 | BUFSIZE = (32 * 1024) 180 | 181 | # Start a select loop writing any input on the child's stdin and reading 182 | # any output from the child's stdout or stderr. 183 | # 184 | # input - String input to write on stdin. May be nil. 185 | # stdin - The write side IO object for the child's stdin stream. 186 | # stdout - The read side IO object for the child's stdout stream. 187 | # stderr - The read side IO object for the child's stderr stream. 188 | # timeout - An optional Numeric specifying the total number of seconds 189 | # the read/write operations should occur for. 190 | # 191 | # Returns an [out, err] tuple where both elements are strings with all 192 | # data written to the stdout and stderr streams, respectively. 193 | # Raises TimeoutExceeded when all data has not been read / written within 194 | # the duration specified in the timeout argument. 195 | # Raises MaximumOutputExceeded when the total number of bytes output 196 | # exceeds the amount specified by the max argument. 197 | def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil) 198 | max = nil if max && max <= 0 199 | @out, @err = '', '' 200 | 201 | # force all string and IO encodings to BINARY under 1.9 for now 202 | if @out.respond_to?(:force_encoding) and stdin.respond_to?(:set_encoding) 203 | [stdin, stdout, stderr].each do |fd| 204 | fd.set_encoding('BINARY', 'BINARY') 205 | end 206 | @out.force_encoding('BINARY') 207 | @err.force_encoding('BINARY') 208 | input = input.dup.force_encoding('BINARY') if input 209 | end 210 | 211 | timeout = nil if timeout && timeout <= 0.0 212 | @runtime = 0.0 213 | start = Time.now 214 | 215 | readers = [stdout, stderr] 216 | writers = 217 | if input 218 | [stdin] 219 | else 220 | stdin.close 221 | [] 222 | end 223 | slice_method = input.respond_to?(:byteslice) ? :byteslice : :slice 224 | t = timeout 225 | 226 | while readers.any? || writers.any? 227 | ready = IO.select(readers, writers, readers + writers, t) 228 | raise TimeoutExceeded if ready.nil? 229 | 230 | # write to stdin stream 231 | ready[1].each do |fd| 232 | begin 233 | boom = nil 234 | size = fd.write_nonblock(input) 235 | input = input.send(slice_method, size..-1) 236 | rescue Errno::EPIPE => boom 237 | rescue Errno::EAGAIN, Errno::EINTR 238 | end 239 | if boom || input.bytesize == 0 240 | stdin.close 241 | writers.delete(stdin) 242 | end 243 | end 244 | 245 | # read from stdout and stderr streams 246 | ready[0].each do |fd| 247 | buf = (fd == stdout) ? @out : @err 248 | begin 249 | buf << fd.readpartial(BUFSIZE) 250 | rescue Errno::EAGAIN, Errno::EINTR 251 | rescue EOFError 252 | readers.delete(fd) 253 | fd.close 254 | end 255 | end 256 | 257 | # keep tabs on the total amount of time we've spent here 258 | @runtime = Time.now - start 259 | if timeout 260 | t = timeout - @runtime 261 | raise TimeoutExceeded if t < 0.0 262 | end 263 | 264 | # maybe we've hit our max output 265 | if max && ready[0].any? && (@out.size + @err.size) > max 266 | raise MaximumOutputExceeded 267 | end 268 | end 269 | 270 | [@out, @err] 271 | end 272 | 273 | # Wait for the child process to exit 274 | # 275 | # Returns the Process::Status object obtained by reaping the process. 276 | def waitpid(pid) 277 | ::Process::waitpid(pid) 278 | $? 279 | end 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # posix-spawn 2 | 3 | `fork(2)` calls slow down as the parent process uses more memory due to the need 4 | to copy page tables. In many common uses of fork(), where it is followed by one 5 | of the exec family of functions to spawn child processes (`Kernel#system`, 6 | `IO::popen`, `Process::spawn`, etc.), it's possible to remove this overhead by using 7 | special process spawning interfaces (`posix_spawn()`, `vfork()`, etc.) 8 | 9 | The posix-spawn library aims to implement a subset of the Ruby 1.9 `Process::spawn` 10 | interface in a way that takes advantage of fast process spawning interfaces when 11 | available and provides sane fallbacks on systems that do not. 12 | 13 | ### FEATURES 14 | 15 | - Fast, constant-time spawn times across a variety of platforms. 16 | - A largish compatible subset of Ruby 1.9's `Process::spawn` interface and 17 | enhanced versions of `Kernel#system`, Kernel#`, etc. under 18 | Ruby >= 1.8.7 (currently MRI only). 19 | - High level `POSIX::Spawn::Child` class for quick (but correct!) 20 | non-streaming IPC scenarios. 21 | 22 | ## BENCHMARKS 23 | 24 | The following benchmarks illustrate time needed to fork/exec a child process at 25 | increasing resident memory sizes on Linux 2.6 and MacOS X. Tests were run using 26 | the [`posix-spawn-benchmark`][pb] program included with the package. 27 | 28 | [pb]: https://github.com/rtomayko/posix-spawn/tree/master/bin 29 | 30 | ### Linux 31 | 32 | ![](https://chart.googleapis.com/chart?chbh=a,5,25&chxr=1,0,36,7&chd=t:5.77,10.37,15.72,18.31,19.73,25.13,26.70,29.31,31.44,35.49|0.86,0.82,1.06,0.99,0.79,1.06,0.84,0.79,0.93,0.94&chxs=1N**%20secs&chs=900x200&chds=0,36&chxl=0:|50%20MB|100%20MB|150%20MB|200%20MB|250%20MB|300%20MB|350%20MB|400%20MB|450%20MB|500%20MB&cht=bvg&chdl=fspawn%20%28fork%2Bexec%29|pspawn%20%28posix_spawn%29&chtt=posix-spawn-benchmark%20--graph%20--count%20500%20--mem-size%20500%20%28x86_64-linux%29&chco=1f77b4,ff7f0e&chf=bg,s,f8f8f8&chxt=x,y#.png) 33 | 34 | `posix_spawn` is faster than `fork+exec`, and executes in constant time when 35 | used with `POSIX_SPAWN_USEVFORK`. 36 | 37 | `fork+exec` is extremely slow for large parent processes. 38 | 39 | ### OSX 40 | 41 | ![](https://chart.googleapis.com/chart?chxl=0:|50%20MB|100%20MB|150%20MB|200%20MB|250%20MB|300%20MB|350%20MB|400%20MB|450%20MB|500%20MB&cht=bvg&chdl=fspawn%20%28fork%2Bexec%29|pspawn%20%28posix_spawn%29&chtt=posix-spawn-benchmark%20--graph%20--count%20500%20--mem-size%20500%20%28i686-darwin10.5.0%29&chco=1f77b4,ff7f0e&chf=bg,s,f8f8f8&chxt=x,y&chbh=a,5,25&chxr=1,0,3,0&chd=t:1.95,2.07,2.56,2.29,2.21,2.32,2.15,2.25,1.96,2.02|0.84,0.97,0.89,0.82,1.13,0.89,0.93,0.81,0.83,0.81&chxs=1N**%20secs&chs=900x200&chds=0,3#.png) 42 | 43 | `posix_spawn` is faster than `fork+exec`, but neither is affected by the size of 44 | the parent process. 45 | 46 | ## USAGE 47 | 48 | This library includes two distinct interfaces: `POSIX::Spawn::spawn`, a lower 49 | level process spawning interface based on the new Ruby 1.9 `Process::spawn` 50 | method, and `POSIX::Spawn::Child`, a higher level class geared toward easy 51 | spawning of processes with simple string based standard input/output/error 52 | stream handling. The former is much more versatile, the latter requires much 53 | less code for certain common scenarios. 54 | 55 | ### POSIX::Spawn::spawn 56 | 57 | The `POSIX::Spawn` module (with help from the accompanying C extension) 58 | implements a subset of the [Ruby 1.9 Process::spawn][ps] interface, largely 59 | through the use of the [IEEE Std 1003.1 `posix_spawn(2)` systems interfaces][po]. 60 | These are widely supported by various UNIX operating systems. 61 | 62 | [ps]: http://www.ruby-doc.org/core-1.9/classes/Process.html#M002230 63 | [po]: http://pubs.opengroup.org/onlinepubs/009695399/functions/posix_spawn.html 64 | 65 | In its simplest form, the `POSIX::Spawn::spawn` method can be used to execute a 66 | child process similar to `Kernel#system`: 67 | 68 | require 'posix/spawn' 69 | pid = POSIX::Spawn::spawn('echo', 'hello world') 70 | stat = Process::waitpid(pid) 71 | 72 | The first line executes `echo` with a single argument and immediately returns 73 | the new process's `pid`. The second line waits for the process to complete and 74 | returns a `Process::Status` object. Note that `spawn` *does not* wait for the 75 | process to finish execution like `system` and does not reap the child's exit 76 | status -- you must call `Process::waitpid` (or equivalent) or the process will 77 | become a zombie. 78 | 79 | The `spawn` method is capable of performing a large number of additional 80 | operations, from setting up the new process's environment, to changing the 81 | child's working directory, to redirecting arbitrary file descriptors. 82 | 83 | See the Ruby 1.9 [`Process::spawn` documentation][ps] for details and the 84 | `STATUS` section below for a full account of the various `Process::spawn` 85 | features supported by `POSIX::Spawn::spawn`. 86 | 87 | ### `system`, `popen4`, and ` 88 | 89 | In addition to the `spawn` method, Ruby 1.9 compatible implementations of 90 | `Kernel#system` and Kernel#\` are provided in the `POSIX::Spawn` 91 | module. The `popen4` method can be used to spawn a process with redirected 92 | stdin, stdout, and stderr objects. 93 | 94 | ### POSIX::Spawn as a Mixin 95 | 96 | The `POSIX::Spawn` module can also be mixed in to classes and modules to include 97 | `spawn` and all utility methods in that namespace: 98 | 99 | require 'posix/spawn' 100 | 101 | class YourGreatClass 102 | include POSIX::Spawn 103 | 104 | def speak(message) 105 | pid = spawn('echo', message) 106 | Process::waitpid(pid) 107 | end 108 | 109 | def calculate(expression) 110 | pid, in, out, err = popen4('bc') 111 | in.write(expression) 112 | in.close 113 | out.read 114 | ensure 115 | [in, out, err].each { |io| io.close if !io.closed? } 116 | Process::waitpid(pid) 117 | end 118 | end 119 | 120 | ### POSIX::Spawn::Child 121 | 122 | The `POSIX::Spawn::Child` class includes logic for executing child processes and 123 | reading/writing from their standard input, output, and error streams. It's 124 | designed to take all input in a single string and provides all output as single 125 | strings and is therefore not well-suited to streaming large quantities of data 126 | in and out of commands. That said, it has some benefits: 127 | 128 | - **Simple** - requires little code for simple stream input and capture. 129 | - **Internally non-blocking** (using `select(2)`) - handles all pipe hang cases 130 | due to exceeding `PIPE_BUF` limits on one or more streams. 131 | - **Potentially portable** - abstracts lower-level process and stream 132 | management APIs so the class can be made to work on platforms like Java and 133 | Windows where UNIX process spawning and stream APIs are not supported. 134 | 135 | `POSIX::Spawn::Child` takes the standard `spawn` arguments when instantiated, 136 | and runs the process to completion after writing all input and reading all 137 | output: 138 | 139 | >> require 'posix/spawn' 140 | >> child = POSIX::Spawn::Child.new('git', '--help') 141 | 142 | Retrieve process output written to stdout / stderr, or inspect the process's 143 | exit status: 144 | 145 | >> child.out 146 | => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..." 147 | >> child.err 148 | => "" 149 | >> child.status 150 | => # 151 | 152 | Use the `:input` option to write data on the new process's stdin immediately 153 | after spawning: 154 | 155 | >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2') 156 | >> child.out 157 | "42\n" 158 | 159 | Additional options can be used to specify the maximum output size (`:max`) and 160 | time of execution (`:timeout`) before the child process is aborted. See the 161 | `POSIX::Spawn::Child` docs for more info. 162 | 163 | #### Reading Partial Results 164 | 165 | `POSIX::Spawn::Child.new` spawns the process immediately when instantiated. 166 | As a result, if it is interrupted by an exception (either from reaching the 167 | maximum output size, the time limit, or another factor), it is not possible to 168 | access the `out` or `err` results because the constructor did not complete. 169 | 170 | If you want to get the `out` and `err` data was available when the process 171 | was interrupted, use the `POSIX::Spawn::Child.build` alternate form to 172 | create the child without immediately spawning the process. Call `exec!` 173 | to run the command at a place where you can catch any exceptions: 174 | 175 | >> child = POSIX::Spawn::Child.build('git', 'log', :max => 100) 176 | >> begin 177 | ?> child.exec! 178 | ?> rescue POSIX::Spawn::MaximumOutputExceeded 179 | ?> # limit was reached 180 | ?> end 181 | >> child.out 182 | "commit fa54abe139fd045bf6dc1cc259c0f4c06a9285bb\n..." 183 | 184 | Please note that when the `MaximumOutputExceeded` exception is raised, the 185 | actual combined `out` and `err` data may be a bit longer than the `:max` 186 | value due to internal buffering. 187 | 188 | ## STATUS 189 | 190 | The `POSIX::Spawn::spawn` method is designed to be as compatible with Ruby 1.9's 191 | `Process::spawn` as possible. Right now, it is a compatible subset. 192 | 193 | These `Process::spawn` arguments are currently supported to any of 194 | `Spawn::spawn`, `Spawn::system`, `Spawn::popen4`, and `Spawn::Child.new`: 195 | 196 | env: hash 197 | name => val : set the environment variable 198 | name => nil : unset the environment variable 199 | command...: 200 | commandline : command line string which is passed to a shell 201 | cmdname, arg1, ... : command name and one or more arguments (no shell) 202 | [cmdname, argv0], arg1, ... : command name, argv[0] and zero or more arguments (no shell) 203 | options: hash 204 | clearing environment variables: 205 | :unsetenv_others => true : clear environment variables except specified by env 206 | :unsetenv_others => false : don't clear (default) 207 | current directory: 208 | :chdir => str : Not thread-safe when using posix_spawn (see below) 209 | process group: 210 | :pgroup => true or 0 : make a new process group 211 | :pgroup => pgid : join to specified process group 212 | :pgroup => nil : don't change the process group (default) 213 | redirection: 214 | key: 215 | FD : single file descriptor in child process 216 | [FD, FD, ...] : multiple file descriptor in child process 217 | value: 218 | FD : redirect to the file descriptor in parent process 219 | :close : close the file descriptor in child process 220 | string : redirect to file with open(string, "r" or "w") 221 | [string] : redirect to file with open(string, File::RDONLY) 222 | [string, open_mode] : redirect to file with open(string, open_mode, 0644) 223 | [string, open_mode, perm] : redirect to file with open(string, open_mode, perm) 224 | FD is one of follows 225 | :in : the file descriptor 0 which is the standard input 226 | :out : the file descriptor 1 which is the standard output 227 | :err : the file descriptor 2 which is the standard error 228 | integer : the file descriptor of specified the integer 229 | io : the file descriptor specified as io.fileno 230 | 231 | These options are currently NOT supported: 232 | 233 | options: hash 234 | resource limit: resourcename is core, cpu, data, etc. See Process.setrlimit. 235 | :rlimit_resourcename => limit 236 | :rlimit_resourcename => [cur_limit, max_limit] 237 | umask: 238 | :umask => int 239 | redirection: 240 | value: 241 | [:child, FD] : redirect to the redirected file descriptor 242 | file descriptor inheritance: close non-redirected non-standard fds (3, 4, 5, ...) or not 243 | :close_others => false : inherit fds (default for system and exec) 244 | :close_others => true : don't inherit (default for spawn and IO.popen) 245 | 246 | The `:chdir` option provided by Posix::Spawn::Child, Posix::Spawn#spawn, 247 | Posix::Spawn#system and Posix::Spawn#popen4 is not thread-safe because 248 | processes spawned with the posix_spawn(2) system call inherit the working 249 | directory of the calling process. The posix-spawn gem works around this 250 | limitation in the system call by changing the working directory of the calling 251 | process immediately before and after spawning the child process. 252 | 253 | ## ACKNOWLEDGEMENTS 254 | 255 | Copyright (c) by 256 | [Ryan Tomayko](http://tomayko.com/about) 257 | and 258 | [Aman Gupta](https://github.com/tmm1). 259 | 260 | See the `COPYING` file for more information on license and redistribution. 261 | -------------------------------------------------------------------------------- /test/test_spawn.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module SpawnImplementationTests 4 | def test_spawn_simple 5 | pid = _spawn('true') 6 | assert_process_exit_ok pid 7 | end 8 | 9 | def test_spawn_with_args 10 | pid = _spawn('true', 'with', 'some stuff') 11 | assert_process_exit_ok pid 12 | end 13 | 14 | def test_spawn_with_shell 15 | pid = _spawn('true && exit 13') 16 | assert_process_exit_status pid, 13 17 | end 18 | 19 | def test_spawn_with_cmdname_and_argv0_tuple 20 | pid = _spawn(['true', 'not-true'], 'some', 'args', 'toooo') 21 | assert_process_exit_ok pid 22 | end 23 | 24 | def test_spawn_with_invalid_argv 25 | assert_raises ArgumentError do 26 | _spawn(['echo','b','c','d']) 27 | end 28 | end 29 | 30 | ## 31 | # Environ 32 | 33 | def test_spawn_inherit_env 34 | ENV['PSPAWN'] = 'parent' 35 | pid = _spawn('test "$PSPAWN" = "parent"') 36 | assert_process_exit_ok pid 37 | ensure 38 | ENV.delete('PSPAWN') 39 | end 40 | 41 | def test_spawn_clean_env 42 | ENV['PSPAWN'] = 'parent' 43 | pid = _spawn({'TEMP'=>'child'}, 'test -z "$PSPAWN" && test "$TEMP" = "child"', :unsetenv_others => true) 44 | assert_process_exit_ok pid 45 | ensure 46 | ENV.delete('PSPAWN') 47 | end 48 | 49 | def test_spawn_set_env 50 | ENV['PSPAWN'] = 'parent' 51 | pid = _spawn({'PSPAWN'=>'child'}, 'test "$PSPAWN" = "child"') 52 | assert_process_exit_ok pid 53 | ensure 54 | ENV.delete('PSPAWN') 55 | end 56 | 57 | def test_spawn_unset_env 58 | ENV['PSPAWN'] = 'parent' 59 | pid = _spawn({'PSPAWN'=>nil}, 'test -z "$PSPAWN"') 60 | assert_process_exit_ok pid 61 | ensure 62 | ENV.delete('PSPAWN') 63 | end 64 | 65 | ## 66 | # FD => :close options 67 | 68 | def test_sanity_of_checking_clone_with_sh 69 | rd, wr = IO.pipe 70 | pid = _spawn("exec 2>/dev/null 9<&#{rd.posix_fileno} || exit 1", rd => rd) 71 | assert_process_exit_status pid, 0 72 | ensure 73 | [rd, wr].each { |fd| fd.close rescue nil } 74 | end 75 | 76 | def test_spawn_close_option_with_symbolic_standard_stream_names 77 | pid = _spawn('true 2>/dev/null 9<&0 || exit 1', :in => :close) 78 | assert_process_exit_status pid, 1 79 | 80 | pid = _spawn('true 2>/dev/null 9>&1 8>&2 || exit 1', 81 | :out => :close, :err => :close) 82 | assert_process_exit_status pid, 1 83 | end 84 | 85 | def test_spawn_close_on_standard_stream_io_object 86 | pid = _spawn('true 2>/dev/null 9<&0 || exit 1', STDIN => :close) 87 | assert_process_exit_status pid, 1 88 | 89 | pid = _spawn('true 2>/dev/null 9>&1 8>&2 || exit 1', 90 | STDOUT => :close, STDOUT => :close) 91 | assert_process_exit_status pid, 1 92 | end 93 | 94 | def test_spawn_close_option_with_fd_number 95 | rd, wr = IO.pipe 96 | pid = _spawn("true 2>/dev/null 9<&#{rd.posix_fileno} || exit 1", rd.posix_fileno => :close) 97 | assert_process_exit_status pid, 1 98 | 99 | assert !rd.closed? 100 | assert !wr.closed? 101 | ensure 102 | [rd, wr].each { |fd| fd.close rescue nil } 103 | end 104 | 105 | def test_spawn_close_option_with_io_object 106 | rd, wr = IO.pipe 107 | pid = _spawn("true 2>/dev/null 9<&#{rd.posix_fileno} || exit 1", rd => :close) 108 | assert_process_exit_status pid, 1 109 | 110 | assert !rd.closed? 111 | assert !wr.closed? 112 | ensure 113 | [rd, wr].each { |fd| fd.close rescue nil } 114 | end 115 | 116 | def test_spawn_close_invalid_fd_raises_exception 117 | pid = _spawn("echo", "hiya", 250 => :close) 118 | assert_process_exit_status pid, 127 119 | rescue Errno::EBADF 120 | # this happens on darwin only. GNU does spawn and exits 127. 121 | end 122 | 123 | def test_spawn_invalid_chdir_raises_exception 124 | pid = _spawn("echo", "hiya", :chdir => "/this/does/not/exist") 125 | # fspawn does chdir in child, so it exits with 127 126 | assert_process_exit_status pid, 127 127 | rescue Errno::ENOENT 128 | # pspawn and native spawn do chdir in parent, so they throw an exception 129 | end 130 | 131 | def test_spawn_closing_multiple_fds_with_array_keys 132 | rd, wr = IO.pipe 133 | pid = _spawn("true 2>/dev/null 9>&#{wr.posix_fileno} || exit 1", [rd, wr, :out] => :close) 134 | assert_process_exit_status pid, 1 135 | ensure 136 | [rd, wr].each { |fd| fd.close rescue nil } 137 | end 138 | 139 | ## 140 | # FD => FD options 141 | 142 | def test_spawn_redirect_fds_with_symbolic_names_and_io_objects 143 | rd, wr = IO.pipe 144 | pid = _spawn("echo", "hello world", :out => wr, rd => :close) 145 | wr.close 146 | output = rd.read 147 | assert_process_exit_ok pid 148 | assert_equal "hello world\n", output 149 | ensure 150 | [rd, wr].each { |fd| fd.close rescue nil } 151 | end 152 | 153 | def test_spawn_redirect_fds_with_fd_numbers 154 | rd, wr = IO.pipe 155 | pid = _spawn("echo", "hello world", 1 => wr.posix_fileno, rd.posix_fileno => :close) 156 | wr.close 157 | output = rd.read 158 | assert_process_exit_ok pid 159 | assert_equal "hello world\n", output 160 | ensure 161 | [rd, wr].each { |fd| fd.close rescue nil } 162 | end 163 | 164 | def test_spawn_redirect_invalid_fds_raises_exception 165 | pid = _spawn("echo", "hiya", 1 => 250) 166 | assert_process_exit_status pid, 127 167 | rescue Errno::EBADF 168 | # this happens on darwin only. GNU does spawn and exits 127. 169 | end 170 | 171 | def test_spawn_redirect_stderr_and_stdout_to_same_fd 172 | rd, wr = IO.pipe 173 | pid = _spawn("echo hello world 1>&2", :err => wr, :out => wr, rd => :close) 174 | wr.close 175 | output = rd.read 176 | assert_process_exit_ok pid 177 | assert_equal "hello world\n", output 178 | ensure 179 | [rd, wr].each { |fd| fd.close rescue nil } 180 | end 181 | 182 | def test_spawn_does_not_close_fd_when_redirecting 183 | pid = _spawn("exec 2>&1", :err => :out) 184 | assert_process_exit_ok pid 185 | end 186 | 187 | # Ruby 1.9 Process::spawn closes all fds by default. To keep an fd open, you 188 | # have to pass it explicitly as fd => fd. 189 | def test_explicitly_passing_an_fd_as_open 190 | rd, wr = IO.pipe 191 | pid = _spawn("exec 9>&#{wr.posix_fileno} || exit 1", wr => wr) 192 | assert_process_exit_ok pid 193 | ensure 194 | [rd, wr].each { |fd| fd.close rescue nil } 195 | end 196 | 197 | ## 198 | # FD => file options 199 | 200 | def test_spawn_redirect_fd_to_file_with_symbolic_name 201 | file = File.expand_path('../test-output', __FILE__) 202 | text = 'redirect_fd_to_file_with_symbolic_name' 203 | pid = _spawn('echo', text, :out => file) 204 | assert_process_exit_ok pid 205 | assert File.exist?(file) 206 | assert_equal "#{text}\n", File.read(file) 207 | ensure 208 | File.unlink(file) rescue nil 209 | end 210 | 211 | def test_spawn_redirect_fd_to_file_with_fd_number 212 | file = File.expand_path('../test-output', __FILE__) 213 | text = 'redirect_fd_to_file_with_fd_number' 214 | pid = _spawn('echo', text, 1 => file) 215 | assert_process_exit_ok pid 216 | assert File.exist?(file) 217 | assert_equal "#{text}\n", File.read(file) 218 | ensure 219 | File.unlink(file) rescue nil 220 | end 221 | 222 | def test_spawn_redirect_fd_to_file_with_io_object 223 | file = File.expand_path('../test-output', __FILE__) 224 | text = 'redirect_fd_to_file_with_io_object' 225 | pid = _spawn('echo', text, STDOUT => file) 226 | assert_process_exit_ok pid 227 | assert File.exist?(file) 228 | assert_equal "#{text}\n", File.read(file) 229 | ensure 230 | File.unlink(file) rescue nil 231 | end 232 | 233 | def test_spawn_redirect_fd_from_file_with_symbolic_name 234 | file = File.expand_path('../test-input', __FILE__) 235 | text = 'redirect_fd_from_file_with_symbolic_name' 236 | File.open(file, 'w') { |fd| fd.write(text) } 237 | 238 | pid = _spawn(%Q{test "$(cat)" = "#{text}"}, :in => file) 239 | assert_process_exit_ok pid 240 | ensure 241 | File.unlink(file) rescue nil 242 | end 243 | 244 | def test_spawn_redirect_fd_from_file_with_fd_number 245 | file = File.expand_path('../test-input', __FILE__) 246 | text = 'redirect_fd_from_file_with_fd_number' 247 | File.open(file, 'w') { |fd| fd.write(text) } 248 | 249 | pid = _spawn(%Q{test "$(cat)" = "#{text}"}, 0 => file) 250 | assert_process_exit_ok pid 251 | ensure 252 | File.unlink(file) rescue nil 253 | end 254 | 255 | def test_spawn_redirect_fd_from_file_with_io_object 256 | file = File.expand_path('../test-input', __FILE__) 257 | text = 'redirect_fd_from_file_with_io_object' 258 | File.open(file, 'w') { |fd| fd.write(text) } 259 | 260 | pid = _spawn(%Q{test "$(cat)" = "#{text}"}, STDIN => file) 261 | assert_process_exit_ok pid 262 | ensure 263 | File.unlink(file) rescue nil 264 | end 265 | 266 | def test_spawn_redirect_fd_to_file_with_symbolic_name_and_flags 267 | file = File.expand_path('../test-output', __FILE__) 268 | text = 'redirect_fd_to_file_with_symbolic_name' 269 | 5.times do 270 | pid = _spawn('echo', text, :out => [file, 'a']) 271 | assert_process_exit_ok pid 272 | end 273 | assert File.exist?(file) 274 | assert_equal "#{text}\n" * 5, File.read(file) 275 | ensure 276 | File.unlink(file) rescue nil 277 | end 278 | 279 | ## 280 | # :pgroup => 281 | 282 | def test_spawn_inherit_pgroup_from_parent_by_default 283 | pgrp = Process.getpgrp 284 | pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)") 285 | assert_process_exit_ok pid 286 | end 287 | 288 | def test_spawn_inherit_pgroup_from_parent_when_nil 289 | pgrp = Process.getpgrp 290 | pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)", :pgroup => nil) 291 | assert_process_exit_ok pid 292 | end 293 | 294 | def test_spawn_new_pgroup_with_true 295 | pid = _spawn("ruby", "-e", "exit(Process.getpgrp == $$ ? 0 : 1)", :pgroup => true) 296 | assert_process_exit_ok pid 297 | end 298 | 299 | def test_spawn_new_pgroup_with_zero 300 | pid = _spawn("ruby", "-e", "exit(Process.getpgrp == $$ ? 0 : 1)", :pgroup => 0) 301 | assert_process_exit_ok pid 302 | end 303 | 304 | def test_spawn_explicit_pgroup 305 | pgrp = Process.getpgrp 306 | pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)", :pgroup => pgrp) 307 | assert_process_exit_ok pid 308 | end 309 | 310 | ## 311 | # Exceptions 312 | 313 | def test_spawn_raises_exception_on_unsupported_options 314 | exception = nil 315 | 316 | assert_raises ArgumentError do 317 | begin 318 | _spawn('echo howdy', :out => '/dev/null', :oops => 'blaahh') 319 | rescue Exception => e 320 | exception = e 321 | raise e 322 | end 323 | end 324 | 325 | assert_match /oops/, exception.message 326 | end 327 | 328 | ## 329 | # Assertion Helpers 330 | 331 | def assert_process_exit_ok(pid) 332 | assert_process_exit_status pid, 0 333 | end 334 | 335 | def assert_process_exit_status(pid, status) 336 | assert pid.to_i > 0, "pid [#{pid}] should be > 0" 337 | chpid = ::Process.wait(pid) 338 | assert_equal chpid, pid 339 | assert_equal status, $?.exitstatus 340 | end 341 | end 342 | 343 | class SpawnTest < Minitest::Test 344 | include POSIX::Spawn 345 | 346 | def test_spawn_methods_exposed_at_module_level 347 | assert POSIX::Spawn.respond_to?(:pspawn) 348 | assert POSIX::Spawn.respond_to?(:_pspawn) 349 | end 350 | 351 | ## 352 | # Options Preprocessing 353 | 354 | def test_extract_process_spawn_arguments_with_options 355 | assert_equal [{}, [['echo', 'echo'], 'hello', 'world'], {:err => :close}], 356 | extract_process_spawn_arguments('echo', 'hello', 'world', :err => :close) 357 | end 358 | 359 | def test_extract_process_spawn_arguments_with_options_and_env 360 | options = {:err => :close} 361 | env = {'X' => 'Y'} 362 | assert_equal [env, [['echo', 'echo'], 'hello world'], options], 363 | extract_process_spawn_arguments(env, 'echo', 'hello world', options) 364 | end 365 | 366 | def test_extract_process_spawn_arguments_with_shell_command 367 | assert_equal [{}, [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'], {}], 368 | extract_process_spawn_arguments('echo hello world') 369 | end 370 | 371 | def test_extract_process_spawn_arguments_with_special_cmdname_argv_tuple 372 | assert_equal [{}, [['echo', 'fuuu'], 'hello world'], {}], 373 | extract_process_spawn_arguments(['echo', 'fuuu'], 'hello world') 374 | end 375 | end 376 | 377 | class PosixSpawnTest < Minitest::Test 378 | include SpawnImplementationTests 379 | def _spawn(*argv) 380 | POSIX::Spawn.pspawn(*argv) 381 | end 382 | end 383 | 384 | class ForkSpawnTest < Minitest::Test 385 | include SpawnImplementationTests 386 | def _spawn(*argv) 387 | POSIX::Spawn.fspawn(*argv) 388 | end 389 | end 390 | 391 | if ::Process::respond_to?(:spawn) 392 | class NativeSpawnTest < Minitest::Test 393 | include SpawnImplementationTests 394 | def _spawn(*argv) 395 | ::Process.spawn(*argv) 396 | end 397 | end 398 | end 399 | -------------------------------------------------------------------------------- /ext/posix-spawn.c: -------------------------------------------------------------------------------- 1 | /* we want GNU extensions like POSIX_SPAWN_USEVFORK */ 2 | #ifndef _GNU_SOURCE 3 | #define _GNU_SOURCE 1 4 | #endif 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef RUBY_VM 17 | #include 18 | #else 19 | #include 20 | #endif 21 | 22 | #ifndef RARRAY_LEN 23 | #define RARRAY_LEN(ary) RARRAY(ary)->len 24 | #endif 25 | #ifndef RARRAY_PTR 26 | #define RARRAY_PTR(ary) RARRAY(ary)->ptr 27 | #endif 28 | #ifndef RHASH_SIZE 29 | #define RHASH_SIZE(hash) RHASH(hash)->tbl->num_entries 30 | #endif 31 | 32 | #ifdef __APPLE__ 33 | #include 34 | #define environ (*_NSGetEnviron()) 35 | #else 36 | extern char **environ; 37 | #endif 38 | 39 | static VALUE rb_mPOSIX; 40 | static VALUE rb_mPOSIXSpawn; 41 | 42 | /* Determine the fd number for a Ruby object VALUE. 43 | * 44 | * obj - This can be any valid Ruby object, but only the following return 45 | * an actual fd number: 46 | * - The symbols :in, :out, or :err for fds 0, 1, or 2. 47 | * - An IO object. (IO#fileno is returned) 48 | * - A Fixnum. 49 | * 50 | * Returns the fd number >= 0 if one could be established, or -1 if the object 51 | * does not map to an fd. 52 | */ 53 | static int 54 | posixspawn_obj_to_fd(VALUE obj) 55 | { 56 | int fd = -1; 57 | switch (TYPE(obj)) { 58 | case T_FIXNUM: 59 | /* Fixnum fd number */ 60 | fd = FIX2INT(obj); 61 | break; 62 | 63 | case T_SYMBOL: 64 | /* (:in|:out|:err) */ 65 | if (SYM2ID(obj) == rb_intern("in")) fd = 0; 66 | else if (SYM2ID(obj) == rb_intern("out")) fd = 1; 67 | else if (SYM2ID(obj) == rb_intern("err")) fd = 2; 68 | break; 69 | 70 | case T_FILE: 71 | /* IO object */ 72 | if (rb_respond_to(obj, rb_intern("posix_fileno"))) { 73 | fd = FIX2INT(rb_funcall(obj, rb_intern("posix_fileno"), 0)); 74 | } else { 75 | fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0)); 76 | } 77 | break; 78 | 79 | case T_OBJECT: 80 | /* some other object */ 81 | if (rb_respond_to(obj, rb_intern("to_io"))) { 82 | obj = rb_funcall(obj, rb_intern("to_io"), 0); 83 | if (rb_respond_to(obj, rb_intern("posix_fileno"))) { 84 | fd = FIX2INT(rb_funcall(obj, rb_intern("posix_fileno"), 0)); 85 | } else { 86 | fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0)); 87 | } 88 | } 89 | break; 90 | } 91 | return fd; 92 | } 93 | 94 | /* 95 | * Hash iterator that sets up the posix_spawn_file_actions_t with addclose 96 | * operations. Only hash pairs whose value is :close are processed. Keys may 97 | * be the :in, :out, :err, an IO object, or a Fixnum fd number. 98 | * 99 | * Returns ST_DELETE when an addclose operation was added; ST_CONTINUE when 100 | * no operation was performed. 101 | */ 102 | static int 103 | posixspawn_file_actions_addclose(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) 104 | { 105 | int fd; 106 | 107 | /* we only care about { (IO|FD|:in|:out|:err) => :close } */ 108 | if (TYPE(val) != T_SYMBOL || SYM2ID(val) != rb_intern("close")) 109 | return ST_CONTINUE; 110 | 111 | fd = posixspawn_obj_to_fd(key); 112 | if (fd >= 0) { 113 | /* raise an exception if 'fd' is invalid */ 114 | if (fcntl(fd, F_GETFD) == -1) { 115 | char error_context[32]; 116 | snprintf(error_context, sizeof(error_context), "when closing fd %d", fd); 117 | rb_sys_fail(error_context); 118 | return ST_DELETE; 119 | } 120 | posix_spawn_file_actions_addclose(fops, fd); 121 | return ST_DELETE; 122 | } else { 123 | return ST_CONTINUE; 124 | } 125 | } 126 | 127 | /* 128 | * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 + 129 | * close operations for all redirects. Only hash pairs whose key and value 130 | * represent fd numbers are processed. 131 | * 132 | * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when 133 | * no operation was performed. 134 | */ 135 | static int 136 | posixspawn_file_actions_adddup2(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) 137 | { 138 | int fd, newfd; 139 | 140 | newfd = posixspawn_obj_to_fd(key); 141 | if (newfd < 0) 142 | return ST_CONTINUE; 143 | 144 | fd = posixspawn_obj_to_fd(val); 145 | if (fd < 0) 146 | return ST_CONTINUE; 147 | 148 | fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC); 149 | fcntl(newfd, F_SETFD, fcntl(newfd, F_GETFD) & ~FD_CLOEXEC); 150 | posix_spawn_file_actions_adddup2(fops, fd, newfd); 151 | return ST_DELETE; 152 | } 153 | 154 | /* 155 | * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 + 156 | * clone operations for all file redirects. Only hash pairs whose key is an 157 | * fd number and value is a valid three-tuple [file, flags, mode] are 158 | * processed. 159 | * 160 | * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when 161 | * no operation was performed. 162 | */ 163 | static int 164 | posixspawn_file_actions_addopen(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) 165 | { 166 | int fd; 167 | char *path; 168 | int oflag; 169 | mode_t mode; 170 | 171 | fd = posixspawn_obj_to_fd(key); 172 | if (fd < 0) 173 | return ST_CONTINUE; 174 | 175 | if (TYPE(val) != T_ARRAY || RARRAY_LEN(val) != 3) 176 | return ST_CONTINUE; 177 | 178 | path = StringValuePtr(RARRAY_PTR(val)[0]); 179 | oflag = FIX2INT(RARRAY_PTR(val)[1]); 180 | mode = FIX2INT(RARRAY_PTR(val)[2]); 181 | 182 | posix_spawn_file_actions_addopen(fops, fd, path, oflag, mode); 183 | return ST_DELETE; 184 | } 185 | 186 | /* 187 | * Main entry point for iterating over the options hash to perform file actions. 188 | * This function dispatches to the addclose and adddup2 functions, stopping once 189 | * an operation was added. 190 | * 191 | * Returns ST_DELETE if one of the handlers performed an operation; ST_CONTINUE 192 | * if not. 193 | */ 194 | static int 195 | posixspawn_file_actions_operations_iter(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) 196 | { 197 | int act; 198 | 199 | act = posixspawn_file_actions_addclose(key, val, fops); 200 | if (act != ST_CONTINUE) return act; 201 | 202 | act = posixspawn_file_actions_adddup2(key, val, fops); 203 | if (act != ST_CONTINUE) return act; 204 | 205 | act = posixspawn_file_actions_addopen(key, val, fops); 206 | if (act != ST_CONTINUE) return act; 207 | 208 | return ST_CONTINUE; 209 | } 210 | 211 | /* 212 | * Initialize the posix_spawn_file_actions_t structure and add operations from 213 | * the options hash. Keys in the options Hash that are processed by handlers are 214 | * removed. 215 | * 216 | * Returns nothing. 217 | */ 218 | static void 219 | posixspawn_file_actions_init(posix_spawn_file_actions_t *fops, VALUE options) 220 | { 221 | posix_spawn_file_actions_init(fops); 222 | rb_hash_foreach(options, posixspawn_file_actions_operations_iter, (VALUE)fops); 223 | } 224 | 225 | /* 226 | * Initialize pgroup related flags in the posix_spawnattr struct based on the 227 | * options Hash. 228 | * 229 | * :pgroup => 0 | true - spawned process is in a new process group with the 230 | * same id as the new process's pid. 231 | * :pgroup => pgid - spawned process is in a new process group with id 232 | * pgid. 233 | * :pgroup => nil - spawned process has the same pgid as the parent 234 | * process (this is the default). 235 | * 236 | * The options Hash is modified in place with the :pgroup key being removed. 237 | */ 238 | static void 239 | posixspawn_set_pgroup(VALUE options, posix_spawnattr_t *pattr, short *pflags) 240 | { 241 | VALUE pgroup_val; 242 | pgroup_val = rb_hash_delete(options, ID2SYM(rb_intern("pgroup"))); 243 | 244 | switch (TYPE(pgroup_val)) { 245 | case T_TRUE: 246 | (*pflags) |= POSIX_SPAWN_SETPGROUP; 247 | posix_spawnattr_setpgroup(pattr, 0); 248 | break; 249 | case T_FIXNUM: 250 | (*pflags) |= POSIX_SPAWN_SETPGROUP; 251 | posix_spawnattr_setpgroup(pattr, FIX2INT(pgroup_val)); 252 | break; 253 | case T_NIL: 254 | break; 255 | default: 256 | rb_raise(rb_eTypeError, ":pgroup option is invalid"); 257 | break; 258 | } 259 | } 260 | 261 | static int 262 | each_env_check_i(VALUE key, VALUE val, VALUE arg) 263 | { 264 | StringValuePtr(key); 265 | if (!NIL_P(val)) StringValuePtr(val); 266 | return ST_CONTINUE; 267 | } 268 | 269 | static int 270 | each_env_i(VALUE key, VALUE val, VALUE arg) 271 | { 272 | const char *name = StringValuePtr(key); 273 | const size_t name_len = strlen(name); 274 | 275 | char **envp = (char **)arg; 276 | size_t i, j; 277 | 278 | for (i = 0; envp[i];) { 279 | const char *ev = envp[i]; 280 | 281 | if (strlen(ev) > name_len && !memcmp(ev, name, name_len) && ev[name_len] == '=') { 282 | for (j = i; envp[j]; ++j) 283 | envp[j] = envp[j + 1]; 284 | continue; 285 | } 286 | i++; 287 | } 288 | 289 | /* 290 | * Insert the new value if we have one. We can assume there is space 291 | * at the end of the list, since ep was preallocated to be big enough 292 | * for the new entries. 293 | */ 294 | if (RTEST(val)) { 295 | char **ep = (char **)arg; 296 | char *cval = StringValuePtr(val); 297 | 298 | size_t cval_len = strlen(cval); 299 | size_t ep_len = name_len + 1 + cval_len + 1; /* +2 for null terminator and '=' separator */ 300 | 301 | /* find the last entry */ 302 | while (*ep != NULL) ++ep; 303 | *ep = malloc(ep_len); 304 | 305 | strncpy(*ep, name, name_len); 306 | (*ep)[name_len] = '='; 307 | strncpy(*ep + name_len + 1, cval, cval_len); 308 | (*ep)[ep_len-1] = 0; 309 | } 310 | 311 | return ST_CONTINUE; 312 | } 313 | 314 | /* 315 | * POSIX::Spawn#_pspawn(env, argv, options) 316 | * 317 | * env - Hash of the new environment. 318 | * argv - The [[cmdname, argv0], argv1, ...] exec array. 319 | * options - The options hash with fd redirect and close operations. 320 | * 321 | * Returns the pid of the newly spawned process. 322 | */ 323 | static VALUE 324 | rb_posixspawn_pspawn(VALUE self, VALUE env, VALUE argv, VALUE options) 325 | { 326 | int i, ret = 0; 327 | char **envp = NULL; 328 | VALUE dirname; 329 | VALUE cmdname; 330 | VALUE unsetenv_others_p = Qfalse; 331 | char *file; 332 | char *cwd = NULL; 333 | pid_t pid; 334 | posix_spawn_file_actions_t fops; 335 | posix_spawnattr_t attr; 336 | sigset_t mask; 337 | short flags = 0; 338 | 339 | /* argv is a [[cmdname, argv0], argv1, argvN, ...] array. */ 340 | if (TYPE(argv) != T_ARRAY || 341 | TYPE(RARRAY_PTR(argv)[0]) != T_ARRAY || 342 | RARRAY_LEN(RARRAY_PTR(argv)[0]) != 2) 343 | rb_raise(rb_eArgError, "Invalid command name"); 344 | 345 | long argc = RARRAY_LEN(argv); 346 | char *cargv[argc + 1]; 347 | 348 | cmdname = RARRAY_PTR(argv)[0]; 349 | file = StringValuePtr(RARRAY_PTR(cmdname)[0]); 350 | 351 | cargv[0] = StringValuePtr(RARRAY_PTR(cmdname)[1]); 352 | for (i = 1; i < argc; i++) 353 | cargv[i] = StringValuePtr(RARRAY_PTR(argv)[i]); 354 | cargv[argc] = NULL; 355 | 356 | if (TYPE(options) == T_HASH) { 357 | unsetenv_others_p = rb_hash_delete(options, ID2SYM(rb_intern("unsetenv_others"))); 358 | } 359 | 360 | if (RTEST(env)) { 361 | /* 362 | * Make sure env is a hash, and all keys and values are strings. 363 | * We do this before allocating space for the new environment to 364 | * prevent a leak when raising an exception after the calloc() below. 365 | */ 366 | Check_Type(env, T_HASH); 367 | rb_hash_foreach(env, each_env_check_i, 0); 368 | 369 | if (RHASH_SIZE(env) > 0) { 370 | int size = 0; 371 | char **new_env; 372 | 373 | char **curr = environ; 374 | if (curr) { 375 | while (*curr != NULL) ++curr, ++size; 376 | } 377 | 378 | if (unsetenv_others_p == Qtrue) { 379 | /* 380 | * ignore the parent's environment by pretending it had 381 | * no entries. the loop below will do nothing. 382 | */ 383 | size = 0; 384 | } 385 | 386 | new_env = calloc(size+RHASH_SIZE(env)+1, sizeof(char*)); 387 | for (i = 0; i < size; i++) { 388 | new_env[i] = strdup(environ[i]); 389 | } 390 | envp = new_env; 391 | 392 | rb_hash_foreach(env, each_env_i, (VALUE)envp); 393 | } 394 | } 395 | 396 | posixspawn_file_actions_init(&fops, options); 397 | posix_spawnattr_init(&attr); 398 | 399 | /* child does not block any signals */ 400 | flags |= POSIX_SPAWN_SETSIGMASK; 401 | sigemptyset(&mask); 402 | posix_spawnattr_setsigmask(&attr, &mask); 403 | 404 | /* Child reverts SIGPIPE handler to the default. */ 405 | flags |= POSIX_SPAWN_SETSIGDEF; 406 | sigaddset(&mask, SIGPIPE); 407 | posix_spawnattr_setsigdefault(&attr, &mask); 408 | 409 | #if defined(POSIX_SPAWN_USEVFORK) || defined(__GLIBC__) 410 | /* Force USEVFORK on GNU libc. If this is undefined, it's probably 411 | * because you forgot to define _GNU_SOURCE at the top of this file. 412 | */ 413 | flags |= POSIX_SPAWN_USEVFORK; 414 | #endif 415 | 416 | /* setup pgroup options */ 417 | posixspawn_set_pgroup(options, &attr, &flags); 418 | 419 | posix_spawnattr_setflags(&attr, flags); 420 | 421 | if (RTEST(dirname = rb_hash_delete(options, ID2SYM(rb_intern("chdir"))))) { 422 | char *new_cwd = StringValuePtr(dirname); 423 | cwd = getcwd(NULL, 0); 424 | if (chdir(new_cwd) == -1) { 425 | free(cwd); 426 | cwd = NULL; 427 | ret = errno; 428 | } 429 | } 430 | 431 | if (ret == 0) { 432 | if (RHASH_SIZE(options) == 0) { 433 | ret = posix_spawnp(&pid, file, &fops, &attr, cargv, envp ? envp : environ); 434 | if (cwd) { 435 | /* Ignore chdir failures here. There's already a child running, so 436 | * raising an exception here would do more harm than good. */ 437 | if (chdir(cwd) == -1) {} 438 | } 439 | } else { 440 | ret = -1; 441 | } 442 | } 443 | 444 | if (cwd) 445 | free(cwd); 446 | 447 | posix_spawn_file_actions_destroy(&fops); 448 | posix_spawnattr_destroy(&attr); 449 | if (envp) { 450 | char **ep = envp; 451 | while (*ep != NULL) free(*ep), ++ep; 452 | free(envp); 453 | } 454 | 455 | if (RHASH_SIZE(options) > 0) { 456 | rb_raise(rb_eArgError, "Invalid option: %s", RSTRING_PTR(rb_inspect(rb_funcall(options, rb_intern("first"), 0)))); 457 | return -1; 458 | } 459 | 460 | if (ret != 0) { 461 | char error_context[PATH_MAX+32]; 462 | snprintf(error_context, sizeof(error_context), "when spawning '%s'", file); 463 | errno = ret; 464 | rb_sys_fail(error_context); 465 | } 466 | 467 | return INT2FIX(pid); 468 | } 469 | 470 | void 471 | Init_posix_spawn_ext() 472 | { 473 | rb_mPOSIX = rb_define_module("POSIX"); 474 | rb_mPOSIXSpawn = rb_define_module_under(rb_mPOSIX, "Spawn"); 475 | rb_define_method(rb_mPOSIXSpawn, "_pspawn", rb_posixspawn_pspawn, 3); 476 | } 477 | 478 | /* vim: set noexpandtab sts=0 ts=4 sw=4: */ 479 | -------------------------------------------------------------------------------- /lib/posix/spawn.rb: -------------------------------------------------------------------------------- 1 | unless RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ 2 | require 'posix_spawn_ext' 3 | end 4 | 5 | require 'posix/spawn/version' 6 | require 'posix/spawn/child' 7 | 8 | class IO 9 | if defined? JRUBY_VERSION 10 | require 'jruby' 11 | def posix_fileno 12 | case self 13 | when STDIN, $stdin 14 | 0 15 | when STDOUT, $stdout 16 | 1 17 | when STDERR, $stderr 18 | 2 19 | else 20 | JRuby.reference(self).getOpenFile.getMainStream.getDescriptor.getChannel.getFDVal 21 | end 22 | end 23 | else 24 | alias :posix_fileno :fileno 25 | end 26 | end 27 | 28 | module POSIX 29 | # The POSIX::Spawn module implements a compatible subset of Ruby 1.9's 30 | # Process::spawn and related methods using the IEEE Std 1003.1 posix_spawn(2) 31 | # system interfaces where available, or a pure Ruby fork/exec based 32 | # implementation when not. 33 | # 34 | # In Ruby 1.9, a versatile new process spawning interface was added 35 | # (Process::spawn) as the foundation for enhanced versions of existing 36 | # process-related methods like Kernel#system, Kernel#`, and IO#popen. These 37 | # methods are backward compatible with their Ruby 1.8 counterparts but 38 | # support a large number of new options. The POSIX::Spawn module implements 39 | # many of these methods with support for most of Ruby 1.9's features. 40 | # 41 | # The argument signatures for all of these methods follow a new convention, 42 | # making it possible to take advantage of Process::spawn features: 43 | # 44 | # spawn([env], command, [argv1, ...], [options]) 45 | # system([env], command, [argv1, ...], [options]) 46 | # popen([[env], command, [argv1, ...]], mode="r", [options]) 47 | # 48 | # The env, command, and options arguments are described below. 49 | # 50 | # == Environment 51 | # 52 | # If a hash is given in the first argument (env), the child process's 53 | # environment becomes a merge of the parent's and any modifications 54 | # specified in the hash. When a value in env is nil, the variable is 55 | # unset in the child: 56 | # 57 | # # set FOO as BAR and unset BAZ. 58 | # spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world') 59 | # 60 | # == Command 61 | # 62 | # The command and optional argvN string arguments specify the command to 63 | # execute and any program arguments. When only command is given and 64 | # includes a space character, the command text is executed by the system 65 | # shell interpreter, as if by: 66 | # 67 | # /bin/sh -c 'command' 68 | # 69 | # When command does not include a space character, or one or more argvN 70 | # arguments are given, the command is executed as if by execve(2) with 71 | # each argument forming the new program's argv. 72 | # 73 | # NOTE: Use of the shell variation is generally discouraged unless you 74 | # indeed want to execute a shell program. Specifying an explicitly argv is 75 | # typically more secure and less error prone in most cases. 76 | # 77 | # == Options 78 | # 79 | # When a hash is given in the last argument (options), it specifies a 80 | # current directory and zero or more fd redirects for the child process. 81 | # 82 | # The :chdir option specifies the current directory. Note that :chdir is not 83 | # thread-safe on systems that provide posix_spawn(2), because it forces a 84 | # temporary change of the working directory of the calling process. 85 | # 86 | # spawn(command, :chdir => "/var/tmp") 87 | # 88 | # The :in, :out, :err, a Fixnum, an IO object or an Array option specify 89 | # fd redirection. For example, stderr can be merged into stdout as follows: 90 | # 91 | # spawn(command, :err => :out) 92 | # spawn(command, 2 => 1) 93 | # spawn(command, STDERR => :out) 94 | # spawn(command, STDERR => STDOUT) 95 | # 96 | # The key is a fd in the newly spawned child process (stderr in this case). 97 | # The value is a fd in the parent process (stdout in this case). 98 | # 99 | # You can also specify a filename for redirection instead of an fd: 100 | # 101 | # spawn(command, :in => "/dev/null") # read mode 102 | # spawn(command, :out => "/dev/null") # write mode 103 | # spawn(command, :err => "log") # write mode 104 | # spawn(command, 3 => "/dev/null") # read mode 105 | # 106 | # When redirecting to stdout or stderr, the files are opened in write mode; 107 | # otherwise, read mode is used. 108 | # 109 | # It's also possible to control the open flags and file permissions 110 | # directly by passing an array value: 111 | # 112 | # spawn(command, :in=>["file"]) # read mode assumed 113 | # spawn(command, :in=>["file", "r"]) # explicit read mode 114 | # spawn(command, :out=>["log", "w"]) # explicit write mode, 0644 assumed 115 | # spawn(command, :out=>["log", "w", 0600]) 116 | # spawn(command, :out=>["log", File::APPEND | File::CREAT, 0600]) 117 | # 118 | # The array is a [filename, open_mode, perms] tuple. open_mode can be a 119 | # string or an integer. When open_mode is omitted or nil, File::RDONLY is 120 | # assumed. The perms element should be an integer. When perms is omitted or 121 | # nil, 0644 is assumed. 122 | # 123 | # The :close It's possible to direct an fd be closed in the child process. This is 124 | # important for implementing `popen`-style logic and other forms of IPC between 125 | # processes using `IO.pipe`: 126 | # 127 | # rd, wr = IO.pipe 128 | # pid = spawn('echo', 'hello world', rd => :close, :stdout => wr) 129 | # wr.close 130 | # output = rd.read 131 | # Process.wait(pid) 132 | # 133 | # == Spawn Implementation 134 | # 135 | # The POSIX::Spawn#spawn method uses the best available implementation given 136 | # the current platform and Ruby version. In order of preference, they are: 137 | # 138 | # 1. The posix_spawn based C extension method (pspawn). 139 | # 2. Process::spawn when available (Ruby 1.9 only). 140 | # 3. A simple pure-Ruby fork/exec based spawn implementation compatible 141 | # with Ruby >= 1.8.7. 142 | # 143 | module Spawn 144 | extend self 145 | 146 | # Spawn a child process with a variety of options using the best 147 | # available implementation for the current platform and Ruby version. 148 | # 149 | # spawn([env], command, [argv1, ...], [options]) 150 | # 151 | # env - Optional hash specifying the new process's environment. 152 | # command - A string command name, or shell program, used to determine the 153 | # program to execute. 154 | # argvN - Zero or more string program arguments (argv). 155 | # options - Optional hash of operations to perform before executing the 156 | # new child process. 157 | # 158 | # Returns the integer pid of the newly spawned process. 159 | # Raises any number of Errno:: exceptions on failure. 160 | def spawn(*args) 161 | if respond_to?(:_pspawn) 162 | pspawn(*args) 163 | elsif ::Process.respond_to?(:spawn) 164 | ::Process::spawn(*args) 165 | else 166 | fspawn(*args) 167 | end 168 | end 169 | 170 | # Spawn a child process with a variety of options using the posix_spawn(2) 171 | # systems interfaces. Supports the standard spawn interface as described in 172 | # the POSIX::Spawn module documentation. 173 | # 174 | # Raises NotImplementedError when the posix_spawn_ext module could not be 175 | # loaded due to lack of platform support. 176 | def pspawn(*args) 177 | env, argv, options = extract_process_spawn_arguments(*args) 178 | raise NotImplementedError unless respond_to?(:_pspawn) 179 | 180 | if defined? JRUBY_VERSION 181 | # On the JVM, changes made to the environment are not propagated down 182 | # to C via get/setenv, so we have to fake it here. 183 | unless options[:unsetenv_others] == true 184 | env = ENV.merge(env) 185 | options[:unsetenv_others] = true 186 | end 187 | end 188 | 189 | _pspawn(env, argv, options) 190 | end 191 | 192 | # Spawn a child process with a variety of options using a pure 193 | # Ruby fork + exec. Supports the standard spawn interface as described in 194 | # the POSIX::Spawn module documentation. 195 | def fspawn(*args) 196 | env, argv, options = extract_process_spawn_arguments(*args) 197 | valid_options = [:chdir, :unsetenv_others, :pgroup] 198 | 199 | if badopt = options.find{ |key,val| !fd?(key) && !valid_options.include?(key) } 200 | raise ArgumentError, "Invalid option: #{badopt[0].inspect}" 201 | elsif !argv.is_a?(Array) || !argv[0].is_a?(Array) || argv[0].size != 2 202 | raise ArgumentError, "Invalid command name" 203 | end 204 | 205 | fork do 206 | begin 207 | # handle FD => {FD, :close, [file,mode,perms]} options 208 | options.each do |key, val| 209 | if fd?(key) 210 | key = fd_to_io(key) 211 | 212 | if fd?(val) 213 | val = fd_to_io(val) 214 | key.reopen(val) 215 | if key.respond_to?(:close_on_exec=) 216 | key.close_on_exec = false 217 | val.close_on_exec = false 218 | end 219 | elsif val == :close 220 | if key.respond_to?(:close_on_exec=) 221 | key.close_on_exec = true 222 | else 223 | key.close 224 | end 225 | elsif val.is_a?(Array) 226 | file, mode_string, perms = *val 227 | key.reopen(File.open(file, mode_string, perms)) 228 | end 229 | end 230 | end 231 | 232 | # setup child environment 233 | ENV.replace({}) if options[:unsetenv_others] == true 234 | env.each { |k, v| ENV[k] = v } 235 | 236 | # { :chdir => '/' } in options means change into that dir 237 | ::Dir.chdir(options[:chdir]) if options[:chdir] 238 | 239 | # { :pgroup => pgid } options 240 | pgroup = options[:pgroup] 241 | pgroup = 0 if pgroup == true 242 | Process::setpgid(0, pgroup) if pgroup 243 | 244 | # do the deed 245 | if RUBY_VERSION =~ /\A1\.8/ 246 | ::Kernel::exec(*argv) 247 | else 248 | argv_and_options = argv + [{:close_others=>false}] 249 | ::Kernel::exec(*argv_and_options) 250 | end 251 | ensure 252 | exit!(127) 253 | end 254 | end 255 | end 256 | 257 | # Executes a command and waits for it to complete. The command's exit 258 | # status is available as $?. Supports the standard spawn interface as 259 | # described in the POSIX::Spawn module documentation. 260 | # 261 | # This method is compatible with Kernel#system. 262 | # 263 | # Returns true if the command returns a zero exit status, or false for 264 | # non-zero exit. 265 | def system(*args) 266 | pid = spawn(*args) 267 | return false if pid <= 0 268 | ::Process.waitpid(pid) 269 | $?.exitstatus == 0 270 | rescue Errno::ENOENT 271 | false 272 | end 273 | 274 | # Executes a command in a subshell using the system's shell interpreter 275 | # and returns anything written to the new process's stdout. This method 276 | # is compatible with Kernel#`. 277 | # 278 | # Returns the String output of the command. 279 | def `(cmd) 280 | r, w = IO.pipe 281 | command_and_args = system_command_prefixes + [cmd, {:out => w, r => :close}] 282 | pid = spawn(*command_and_args) 283 | 284 | if pid > 0 285 | w.close 286 | out = r.read 287 | ::Process.waitpid(pid) 288 | out 289 | else 290 | '' 291 | end 292 | ensure 293 | [r, w].each{ |io| io.close rescue nil } 294 | end 295 | 296 | # Spawn a child process with all standard IO streams piped in and out of 297 | # the spawning process. Supports the standard spawn interface as described 298 | # in the POSIX::Spawn module documentation. 299 | # 300 | # Returns a [pid, stdin, stdout, stderr] tuple, where pid is the new 301 | # process's pid, stdin is a writeable IO object, and stdout / stderr are 302 | # readable IO objects. The caller should take care to close all IO objects 303 | # when finished and the child process's status must be collected by a call 304 | # to Process::waitpid or equivalent. 305 | def popen4(*argv) 306 | # create some pipes (see pipe(2) manual -- the ruby docs suck) 307 | ird, iwr = IO.pipe 308 | ord, owr = IO.pipe 309 | erd, ewr = IO.pipe 310 | 311 | # spawn the child process with either end of pipes hooked together 312 | opts = 313 | ((argv.pop if argv[-1].is_a?(Hash)) || {}).merge( 314 | # redirect fds # close other sides 315 | :in => ird, iwr => :close, 316 | :out => owr, ord => :close, 317 | :err => ewr, erd => :close 318 | ) 319 | pid = spawn(*(argv + [opts])) 320 | 321 | [pid, iwr, ord, erd] 322 | ensure 323 | # we're in the parent, close child-side fds 324 | [ird, owr, ewr].each { |fd| fd.close if fd } 325 | end 326 | 327 | ## 328 | # Process::Spawn::Child Exceptions 329 | 330 | # Exception raised when the total number of bytes output on the command's 331 | # stderr and stdout streams exceeds the maximum output size (:max option). 332 | # Currently 333 | class MaximumOutputExceeded < StandardError 334 | end 335 | 336 | # Exception raised when timeout is exceeded. 337 | class TimeoutExceeded < StandardError 338 | end 339 | 340 | private 341 | 342 | # Turns the various varargs incantations supported by Process::spawn into a 343 | # simple [env, argv, options] tuple. This just makes life easier for the 344 | # extension functions. 345 | # 346 | # The following method signature is supported: 347 | # Process::spawn([env], command, ..., [options]) 348 | # 349 | # The env and options hashes are optional. The command may be a variable 350 | # number of strings or an Array full of strings that make up the new process's 351 | # argv. 352 | # 353 | # Returns an [env, argv, options] tuple. All elements are guaranteed to be 354 | # non-nil. When no env or options are given, empty hashes are returned. 355 | def extract_process_spawn_arguments(*args) 356 | # pop the options hash off the end if it's there 357 | options = 358 | if args[-1].respond_to?(:to_hash) 359 | args.pop.to_hash 360 | else 361 | {} 362 | end 363 | flatten_process_spawn_options!(options) 364 | normalize_process_spawn_redirect_file_options!(options) 365 | 366 | # shift the environ hash off the front if it's there and account for 367 | # possible :env key in options hash. 368 | env = 369 | if args[0].respond_to?(:to_hash) 370 | args.shift.to_hash 371 | else 372 | {} 373 | end 374 | env.merge!(options.delete(:env)) if options.key?(:env) 375 | 376 | # remaining arguments are the argv supporting a number of variations. 377 | argv = adjust_process_spawn_argv(args) 378 | 379 | [env, argv, options] 380 | end 381 | 382 | # Convert { [fd1, fd2, ...] => (:close|fd) } options to individual keys, 383 | # like: { fd1 => :close, fd2 => :close }. This just makes life easier for the 384 | # spawn implementations. 385 | # 386 | # options - The options hash. This is modified in place. 387 | # 388 | # Returns the modified options hash. 389 | def flatten_process_spawn_options!(options) 390 | options.to_a.each do |key, value| 391 | if key.respond_to?(:to_ary) 392 | key.to_ary.each { |fd| options[fd] = value } 393 | options.delete(key) 394 | end 395 | end 396 | end 397 | 398 | # Mapping of string open modes to integer oflag versions. 399 | OFLAGS = { 400 | "r" => File::RDONLY, 401 | "r+" => File::RDWR | File::CREAT, 402 | "w" => File::WRONLY | File::CREAT | File::TRUNC, 403 | "w+" => File::RDWR | File::CREAT | File::TRUNC, 404 | "a" => File::WRONLY | File::APPEND | File::CREAT, 405 | "a+" => File::RDWR | File::APPEND | File::CREAT 406 | } 407 | 408 | # Convert variations of redirecting to a file to a standard tuple. 409 | # 410 | # :in => '/some/file' => ['/some/file', 'r', 0644] 411 | # :out => '/some/file' => ['/some/file', 'w', 0644] 412 | # :err => '/some/file' => ['/some/file', 'w', 0644] 413 | # STDIN => '/some/file' => ['/some/file', 'r', 0644] 414 | # 415 | # Returns the modified options hash. 416 | def normalize_process_spawn_redirect_file_options!(options) 417 | options.to_a.each do |key, value| 418 | next if !fd?(key) 419 | 420 | # convert string and short array values to 421 | if value.respond_to?(:to_str) 422 | value = default_file_reopen_info(key, value) 423 | elsif value.respond_to?(:to_ary) && value.size < 3 424 | defaults = default_file_reopen_info(key, value[0]) 425 | value += defaults[value.size..-1] 426 | else 427 | value = nil 428 | end 429 | 430 | # replace string open mode flag maybe and replace original value 431 | if value 432 | value[1] = OFLAGS[value[1]] if value[1].respond_to?(:to_str) 433 | options[key] = value 434 | end 435 | end 436 | end 437 | 438 | # The default [file, flags, mode] tuple for a given fd and filename. The 439 | # default flags vary based on the what fd is being redirected. stdout and 440 | # stderr default to write, while stdin and all other fds default to read. 441 | # 442 | # fd - The file descriptor that is being redirected. This may be an IO 443 | # object, integer fd number, or :in, :out, :err for one of the standard 444 | # streams. 445 | # file - The string path to the file that fd should be redirected to. 446 | # 447 | # Returns a [file, flags, mode] tuple. 448 | def default_file_reopen_info(fd, file) 449 | case fd 450 | when :in, STDIN, $stdin, 0 451 | [file, "r", 0644] 452 | when :out, STDOUT, $stdout, 1 453 | [file, "w", 0644] 454 | when :err, STDERR, $stderr, 2 455 | [file, "w", 0644] 456 | else 457 | [file, "r", 0644] 458 | end 459 | end 460 | 461 | # Determine whether object is fd-like. 462 | # 463 | # Returns true if object is an instance of IO, Fixnum >= 0, or one of the 464 | # the symbolic names :in, :out, or :err. 465 | def fd?(object) 466 | case object 467 | when Fixnum 468 | object >= 0 469 | when :in, :out, :err, STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, IO 470 | true 471 | else 472 | object.respond_to?(:to_io) && !object.to_io.nil? 473 | end 474 | end 475 | 476 | # Convert a fd identifier to an IO object. 477 | # 478 | # Returns nil or an instance of IO. 479 | def fd_to_io(object) 480 | case object 481 | when STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr 482 | object 483 | when :in, 0 484 | STDIN 485 | when :out, 1 486 | STDOUT 487 | when :err, 2 488 | STDERR 489 | when Fixnum 490 | object >= 0 ? IO.for_fd(object) : nil 491 | when IO 492 | object 493 | else 494 | object.respond_to?(:to_io) ? object.to_io : nil 495 | end 496 | end 497 | 498 | # Derives the shell command to use when running the spawn. 499 | # 500 | # On a Windows machine, this will yield: 501 | # [['cmd.exe', 'cmd.exe'], '/c'] 502 | # Note: 'cmd.exe' is used if the COMSPEC environment variable 503 | # is not specified. If you would like to use something other 504 | # than 'cmd.exe', specify its path in ENV['COMSPEC'] 505 | # 506 | # On all other systems, this will yield: 507 | # [['/bin/sh', '/bin/sh'], '-c'] 508 | # 509 | # Returns a platform-specific [[, ], ] array. 510 | def system_command_prefixes 511 | if RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ 512 | sh = ENV['COMSPEC'] || 'cmd.exe' 513 | [[sh, sh], '/c'] 514 | else 515 | [['/bin/sh', '/bin/sh'], '-c'] 516 | end 517 | end 518 | 519 | # Converts the various supported command argument variations into a 520 | # standard argv suitable for use with exec. This includes detecting commands 521 | # to be run through the shell (single argument strings with spaces). 522 | # 523 | # The args array may follow any of these variations: 524 | # 525 | # 'true' => [['true', 'true']] 526 | # 'echo', 'hello', 'world' => [['echo', 'echo'], 'hello', 'world'] 527 | # 'echo hello world' => [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'] 528 | # ['echo', 'fuuu'], 'hello' => [['echo', 'fuuu'], 'hello'] 529 | # 530 | # Returns a [[cmdname, argv0], argv1, ...] array. 531 | def adjust_process_spawn_argv(args) 532 | if args.size == 1 && args[0] =~ /[ |>]/ 533 | # single string with these characters means run it through the shell 534 | command_and_args = system_command_prefixes + [args[0]] 535 | [*command_and_args] 536 | elsif !args[0].respond_to?(:to_ary) 537 | # [argv0, argv1, ...] 538 | [[args[0], args[0]], *args[1..-1]] 539 | else 540 | # [[cmdname, argv0], argv1, ...] 541 | args 542 | end 543 | end 544 | end 545 | end 546 | --------------------------------------------------------------------------------