mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
f5c7fc495a
The process group id (/proc/[pid]/stat 5th field) is 0 in the Travis arm64 environment. This is a case where it is available. $ cat /proc/4543/stat 4543 (ruby) S 4525 4525 1384 34818 4525 4194304 37443 1754841 0 0 366 105 2291 391 20 0 3 0 1381328 1428127744 11475 18446744073709551615 94195983785984 94195986670225 140728933833312 0 0 0 0 0 1107394127 0 0 0 17 2 0 0 1 0 0 94195987686512 94195987708942 94196017770496 140728933835483 140728933835595 140728933835595 140728933842904 0 This is a case where it is not available in Travis arm64 environment. $ cat /proc/19179/stat 19179 (ruby) S 19160 0 0 0 -1 4194560 37618 1710547 313 163 770 665 5206 1439 20 0 2 0 17529566 1196347392 10319 18446744073709551615 187650811428864 187650815023116 281474602721280 0 0 0 0 4096 1107390031 0 0 0 17 22 0 0 0 0 0 187650815091456 187650815114064 187651414974464 281474602725080 281474602725211 281474602725211 281474602729420 0 See "man proc" for detail.
712 lines
22 KiB
Ruby
712 lines
22 KiB
Ruby
require_relative '../../spec_helper'
|
|
require_relative 'fixtures/common'
|
|
|
|
newline = "\n"
|
|
platform_is :windows do
|
|
newline = "\r\n"
|
|
end
|
|
|
|
describe :process_spawn_does_not_close_std_streams, shared: true do
|
|
it "does not close STDIN" do
|
|
code = "puts STDIN.read"
|
|
cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
|
|
ruby_exe(cmd, args: "< #{fixture(__FILE__, "in.txt")} > #{@name}")
|
|
File.binread(@name).should == %[stdin#{newline}]
|
|
end
|
|
|
|
it "does not close STDOUT" do
|
|
code = "STDOUT.puts 'hello'"
|
|
cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
|
|
ruby_exe(cmd, args: "> #{@name}")
|
|
File.binread(@name).should == "hello#{newline}"
|
|
end
|
|
|
|
it "does not close STDERR" do
|
|
code = "STDERR.puts 'hello'"
|
|
cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
|
|
ruby_exe(cmd, args: "2> #{@name}")
|
|
File.binread(@name).should =~ /hello#{newline}/
|
|
end
|
|
end
|
|
|
|
describe "Process.spawn" do
|
|
ProcessSpecs.use_system_ruby(self)
|
|
|
|
before :each do
|
|
@name = tmp("process_spawn.txt")
|
|
@var = "$FOO"
|
|
platform_is :windows do
|
|
@var = "%FOO%"
|
|
end
|
|
end
|
|
|
|
after :each do
|
|
rm_r @name
|
|
end
|
|
|
|
it "executes the given command" do
|
|
-> { Process.wait Process.spawn("echo spawn") }.should output_to_fd("spawn\n")
|
|
end
|
|
|
|
it "returns the process ID of the new process as a Fixnum" do
|
|
pid = Process.spawn(*ruby_exe, "-e", "exit")
|
|
Process.wait pid
|
|
pid.should be_an_instance_of(Fixnum)
|
|
end
|
|
|
|
it "returns immediately" do
|
|
start = Time.now
|
|
pid = Process.spawn(*ruby_exe, "-e", "sleep 10")
|
|
(Time.now - start).should < 5
|
|
Process.kill :KILL, pid
|
|
Process.wait pid
|
|
end
|
|
|
|
# argv processing
|
|
|
|
describe "with a single argument" do
|
|
platform_is_not :windows do
|
|
it "subjects the specified command to shell expansion" do
|
|
-> { Process.wait Process.spawn("echo *") }.should_not output_to_fd("*\n")
|
|
end
|
|
|
|
it "creates an argument array with shell parsing semantics for whitespace" do
|
|
-> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n")
|
|
end
|
|
end
|
|
|
|
platform_is :windows do
|
|
# There is no shell expansion on Windows
|
|
it "does not subject the specified command to shell expansion on Windows" do
|
|
-> { Process.wait Process.spawn("echo *") }.should output_to_fd("*\n")
|
|
end
|
|
|
|
it "does not create an argument array with shell parsing semantics for whitespace on Windows" do
|
|
-> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n")
|
|
end
|
|
end
|
|
|
|
it "calls #to_str to convert the argument to a String" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("echo foo")
|
|
-> { Process.wait Process.spawn(o) }.should output_to_fd("foo\n")
|
|
end
|
|
|
|
it "raises an ArgumentError if the command includes a null byte" do
|
|
-> { Process.spawn "\000" }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises a TypeError if the argument does not respond to #to_str" do
|
|
-> { Process.spawn :echo }.should raise_error(TypeError)
|
|
end
|
|
end
|
|
|
|
describe "with multiple arguments" do
|
|
it "does not subject the arguments to shell expansion" do
|
|
-> { Process.wait Process.spawn("echo", "*") }.should output_to_fd("*\n")
|
|
end
|
|
|
|
it "preserves whitespace in passed arguments" do
|
|
out = "a b c d\n"
|
|
platform_is :windows do
|
|
# The echo command on Windows takes quotes literally
|
|
out = "\"a b c d\"\n"
|
|
end
|
|
-> { Process.wait Process.spawn("echo", "a b c d") }.should output_to_fd(out)
|
|
end
|
|
|
|
it "calls #to_str to convert the arguments to Strings" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("foo")
|
|
-> { Process.wait Process.spawn("echo", o) }.should output_to_fd("foo\n")
|
|
end
|
|
|
|
it "raises an ArgumentError if an argument includes a null byte" do
|
|
-> { Process.spawn "echo", "\000" }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises a TypeError if an argument does not respond to #to_str" do
|
|
-> { Process.spawn "echo", :foo }.should raise_error(TypeError)
|
|
end
|
|
end
|
|
|
|
describe "with a command array" do
|
|
it "uses the first element as the command name and the second as the argv[0] value" do
|
|
platform_is_not :windows do
|
|
-> { Process.wait Process.spawn(["/bin/sh", "argv_zero"], "-c", "echo $0") }.should output_to_fd("argv_zero\n")
|
|
end
|
|
platform_is :windows do
|
|
-> { Process.wait Process.spawn(["cmd.exe", "/C"], "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n")
|
|
end
|
|
end
|
|
|
|
it "does not subject the arguments to shell expansion" do
|
|
-> { Process.wait Process.spawn(["echo", "echo"], "*") }.should output_to_fd("*\n")
|
|
end
|
|
|
|
it "preserves whitespace in passed arguments" do
|
|
out = "a b c d\n"
|
|
platform_is :windows do
|
|
# The echo command on Windows takes quotes literally
|
|
out = "\"a b c d\"\n"
|
|
end
|
|
-> { Process.wait Process.spawn(["echo", "echo"], "a b c d") }.should output_to_fd(out)
|
|
end
|
|
|
|
it "calls #to_ary to convert the argument to an Array" do
|
|
o = mock("to_ary")
|
|
platform_is_not :windows do
|
|
o.should_receive(:to_ary).and_return(["/bin/sh", "argv_zero"])
|
|
-> { Process.wait Process.spawn(o, "-c", "echo $0") }.should output_to_fd("argv_zero\n")
|
|
end
|
|
platform_is :windows do
|
|
o.should_receive(:to_ary).and_return(["cmd.exe", "/C"])
|
|
-> { Process.wait Process.spawn(o, "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n")
|
|
end
|
|
end
|
|
|
|
it "calls #to_str to convert the first element to a String" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("echo")
|
|
-> { Process.wait Process.spawn([o, "echo"], "foo") }.should output_to_fd("foo\n")
|
|
end
|
|
|
|
it "calls #to_str to convert the second element to a String" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("echo")
|
|
-> { Process.wait Process.spawn(["echo", o], "foo") }.should output_to_fd("foo\n")
|
|
end
|
|
|
|
it "raises an ArgumentError if the Array does not have exactly two elements" do
|
|
-> { Process.spawn([]) }.should raise_error(ArgumentError)
|
|
-> { Process.spawn([:a]) }.should raise_error(ArgumentError)
|
|
-> { Process.spawn([:a, :b, :c]) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError if the Strings in the Array include a null byte" do
|
|
-> { Process.spawn ["\000", "echo"] }.should raise_error(ArgumentError)
|
|
-> { Process.spawn ["echo", "\000"] }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises a TypeError if an element in the Array does not respond to #to_str" do
|
|
-> { Process.spawn ["echo", :echo] }.should raise_error(TypeError)
|
|
-> { Process.spawn [:echo, "echo"] }.should raise_error(TypeError)
|
|
end
|
|
end
|
|
|
|
# env handling
|
|
|
|
after :each do
|
|
ENV.delete("FOO")
|
|
end
|
|
|
|
it "sets environment variables in the child environment" do
|
|
Process.wait Process.spawn({"FOO" => "BAR"}, "echo #{@var}>#{@name}")
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
|
|
it "unsets environment variables whose value is nil" do
|
|
ENV["FOO"] = "BAR"
|
|
Process.wait Process.spawn({"FOO" => nil}, "echo #{@var}>#{@name}")
|
|
expected = "\n"
|
|
platform_is :windows do
|
|
# Windows does not expand the variable if it is unset
|
|
expected = "#{@var}\n"
|
|
end
|
|
File.read(@name).should == expected
|
|
end
|
|
|
|
it "calls #to_hash to convert the environment" do
|
|
o = mock("to_hash")
|
|
o.should_receive(:to_hash).and_return({"FOO" => "BAR"})
|
|
Process.wait Process.spawn(o, "echo #{@var}>#{@name}")
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
|
|
it "calls #to_str to convert the environment keys" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("FOO")
|
|
Process.wait Process.spawn({o => "BAR"}, "echo #{@var}>#{@name}")
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
|
|
it "calls #to_str to convert the environment values" do
|
|
o = mock("to_str")
|
|
o.should_receive(:to_str).and_return("BAR")
|
|
Process.wait Process.spawn({"FOO" => o}, "echo #{@var}>#{@name}")
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
|
|
it "raises an ArgumentError if an environment key includes an equals sign" do
|
|
-> do
|
|
Process.spawn({"FOO=" => "BAR"}, "echo #{@var}>#{@name}")
|
|
end.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError if an environment key includes a null byte" do
|
|
-> do
|
|
Process.spawn({"\000" => "BAR"}, "echo #{@var}>#{@name}")
|
|
end.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError if an environment value includes a null byte" do
|
|
-> do
|
|
Process.spawn({"FOO" => "\000"}, "echo #{@var}>#{@name}")
|
|
end.should raise_error(ArgumentError)
|
|
end
|
|
|
|
# :unsetenv_others
|
|
|
|
before :each do
|
|
@minimal_env = {
|
|
"PATH" => ENV["PATH"],
|
|
"HOME" => ENV["HOME"]
|
|
}
|
|
@common_env_spawn_args = [@minimal_env, "echo #{@var}>#{@name}"]
|
|
end
|
|
|
|
platform_is_not :windows do
|
|
it "unsets other environment variables when given a true :unsetenv_others option" do
|
|
ENV["FOO"] = "BAR"
|
|
Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: true)
|
|
$?.success?.should be_true
|
|
File.read(@name).should == "\n"
|
|
end
|
|
end
|
|
|
|
it "does not unset other environment variables when given a false :unsetenv_others option" do
|
|
ENV["FOO"] = "BAR"
|
|
Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: false)
|
|
$?.success?.should be_true
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
|
|
platform_is_not :windows do
|
|
it "does not unset environment variables included in the environment hash" do
|
|
env = @minimal_env.merge({"FOO" => "BAR"})
|
|
Process.wait Process.spawn(env, "echo #{@var}>#{@name}", unsetenv_others: true)
|
|
$?.success?.should be_true
|
|
File.read(@name).should == "BAR\n"
|
|
end
|
|
end
|
|
|
|
# :pgroup
|
|
|
|
platform_is_not :windows do
|
|
it "joins the current process group by default" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"))
|
|
end.should output_to_fd(Process.getpgid(Process.pid).to_s)
|
|
end
|
|
|
|
it "joins the current process if pgroup: false" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: false)
|
|
end.should output_to_fd(Process.getpgid(Process.pid).to_s)
|
|
end
|
|
|
|
it "joins the current process if pgroup: nil" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: nil)
|
|
end.should output_to_fd(Process.getpgid(Process.pid).to_s)
|
|
end
|
|
|
|
it "joins a new process group if pgroup: true" do
|
|
process = -> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: true)
|
|
end
|
|
|
|
process.should_not output_to_fd(Process.getpgid(Process.pid).to_s)
|
|
process.should output_to_fd(/\d+/)
|
|
end
|
|
|
|
it "joins a new process group if pgroup: 0" do
|
|
process = -> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: 0)
|
|
end
|
|
|
|
process.should_not output_to_fd(Process.getpgid(Process.pid).to_s)
|
|
process.should output_to_fd(/\d+/)
|
|
end
|
|
|
|
it "joins the specified process group if pgroup: pgid" do
|
|
pgid = Process.getpgid(Process.pid)
|
|
# The process group is not available on all platforms.
|
|
# See "man proc" - /proc/[pid]/stat - (5) pgrp
|
|
# In Travis arm64 environment, the value is 0.
|
|
#
|
|
# $ cat /proc/[pid]/stat
|
|
# 19179 (ruby) S 19160 0 0 ...
|
|
unless pgid.zero?
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: pgid)
|
|
end.should output_to_fd(pgid.to_s)
|
|
else
|
|
skip "The process group is not available."
|
|
end
|
|
end
|
|
|
|
it "raises an ArgumentError if given a negative :pgroup option" do
|
|
-> { Process.spawn("echo", pgroup: -1) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises a TypeError if given a symbol as :pgroup option" do
|
|
-> { Process.spawn("echo", pgroup: :true) }.should raise_error(TypeError)
|
|
end
|
|
end
|
|
|
|
platform_is :windows do
|
|
it "raises an ArgumentError if given :pgroup option" do
|
|
-> { Process.spawn("echo", pgroup: false) }.should raise_error(ArgumentError)
|
|
end
|
|
end
|
|
|
|
# :rlimit_core
|
|
# :rlimit_cpu
|
|
# :rlimit_data
|
|
|
|
# :chdir
|
|
|
|
it "uses the current working directory as its working directory" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Dir.pwd"))
|
|
end.should output_to_fd(Dir.pwd)
|
|
end
|
|
|
|
describe "when passed :chdir" do
|
|
before do
|
|
@dir = tmp("spawn_chdir", false)
|
|
Dir.mkdir @dir
|
|
end
|
|
|
|
after do
|
|
rm_r @dir
|
|
end
|
|
|
|
it "changes to the directory passed for :chdir" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: @dir)
|
|
end.should output_to_fd(@dir)
|
|
end
|
|
|
|
it "calls #to_path to convert the :chdir value" do
|
|
dir = mock("spawn_to_path")
|
|
dir.should_receive(:to_path).and_return(@dir)
|
|
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: dir)
|
|
end.should output_to_fd(@dir)
|
|
end
|
|
end
|
|
|
|
# chdir
|
|
|
|
platform_is :linux do
|
|
describe "inside Dir.chdir" do
|
|
def child_pids(pid)
|
|
`pgrep -P #{pid}`.lines.map { |child| Integer(child) }
|
|
end
|
|
|
|
it "does not create extra process without chdir" do
|
|
pid = Process.spawn("sleep 10")
|
|
begin
|
|
child_pids(pid).size.should == 0
|
|
ensure
|
|
Process.kill("TERM", pid)
|
|
Process.wait(pid)
|
|
end
|
|
end
|
|
|
|
it "kills extra chdir processes" do
|
|
pid = nil
|
|
Dir.chdir("/tmp") do
|
|
pid = Process.spawn("sleep 10")
|
|
end
|
|
|
|
children = child_pids(pid)
|
|
children.size.should <= 1
|
|
|
|
Process.kill("TERM", pid)
|
|
Process.wait(pid)
|
|
|
|
if children.size > 0
|
|
# wait a bit for children to die
|
|
sleep(1)
|
|
|
|
children.each do |child|
|
|
-> do
|
|
Process.kill("TERM", child)
|
|
end.should raise_error(Errno::ESRCH)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# :umask
|
|
|
|
it "uses the current umask by default" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print File.umask"))
|
|
end.should output_to_fd(File.umask.to_s)
|
|
end
|
|
|
|
platform_is_not :windows do
|
|
it "sets the umask if given the :umask option" do
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print File.umask"), umask: 146)
|
|
end.should output_to_fd("146")
|
|
end
|
|
end
|
|
|
|
# redirection
|
|
|
|
it "redirects STDOUT to the given file descriptor if out: Fixnum" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn("echo glark", out: file.fileno)
|
|
end.should output_to_fd("glark\n", file)
|
|
end
|
|
end
|
|
|
|
it "redirects STDOUT to the given file if out: IO" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn("echo glark", out: file)
|
|
end.should output_to_fd("glark\n", file)
|
|
end
|
|
end
|
|
|
|
it "redirects STDOUT to the given file if out: String" do
|
|
Process.wait Process.spawn("echo glark", out: @name)
|
|
File.read(@name).should == "glark\n"
|
|
end
|
|
|
|
it "redirects STDOUT to the given file if out: [String name, String mode]" do
|
|
Process.wait Process.spawn("echo glark", out: [@name, 'w'])
|
|
File.read(@name).should == "glark\n"
|
|
end
|
|
|
|
it "redirects STDERR to the given file descriptor if err: Fixnum" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn("echo glark>&2", err: file.fileno)
|
|
end.should output_to_fd("glark\n", file)
|
|
end
|
|
end
|
|
|
|
it "redirects STDERR to the given file descriptor if err: IO" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn("echo glark>&2", err: file)
|
|
end.should output_to_fd("glark\n", file)
|
|
end
|
|
end
|
|
|
|
it "redirects STDERR to the given file if err: String" do
|
|
Process.wait Process.spawn("echo glark>&2", err: @name)
|
|
File.read(@name).should == "glark\n"
|
|
end
|
|
|
|
it "redirects STDERR to child STDOUT if :err => [:child, :out]" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn("echo glark>&2", :out => file, :err => [:child, :out])
|
|
end.should output_to_fd("glark\n", file)
|
|
end
|
|
end
|
|
|
|
it "redirects both STDERR and STDOUT to the given file descriptor" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"),
|
|
[:out, :err] => file.fileno)
|
|
end.should output_to_fd("glarkbang", file)
|
|
end
|
|
end
|
|
|
|
it "redirects both STDERR and STDOUT to the given IO" do
|
|
File.open(@name, 'w') do |file|
|
|
-> do
|
|
Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"),
|
|
[:out, :err] => file)
|
|
end.should output_to_fd("glarkbang", file)
|
|
end
|
|
end
|
|
|
|
it "redirects both STDERR and STDOUT at the time to the given name" do
|
|
touch @name
|
|
Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), [:out, :err] => @name)
|
|
File.read(@name).should == "glarkbang"
|
|
end
|
|
|
|
# :close_others
|
|
|
|
platform_is_not :windows do
|
|
context "defaults :close_others to" do
|
|
ruby_version_is ""..."2.6" do
|
|
it "true" do
|
|
IO.pipe do |r, w|
|
|
w.close_on_exec = false
|
|
code = "begin; IO.new(#{w.fileno}).close; rescue Errno::EBADF; puts 'not inherited'; end"
|
|
Process.wait Process.spawn(ruby_cmd(code), :out => @name)
|
|
File.read(@name).should == "not inherited\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
ruby_version_is "2.6" do
|
|
it "false" do
|
|
IO.pipe do |r, w|
|
|
w.close_on_exec = false
|
|
code = "io = IO.new(#{w.fileno}); io.puts('inherited'); io.close"
|
|
pid = Process.spawn(ruby_cmd(code))
|
|
w.close
|
|
Process.wait(pid)
|
|
r.read.should == "inherited\n"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when passed close_others: true" do
|
|
before :each do
|
|
@options = { close_others: true }
|
|
end
|
|
|
|
it "closes file descriptors >= 3 in the child process even if fds are set close_on_exec=false" do
|
|
touch @name
|
|
IO.pipe do |r, w|
|
|
r.close_on_exec = false
|
|
w.close_on_exec = false
|
|
|
|
begin
|
|
pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options)
|
|
w.close
|
|
r.read(1).should == nil
|
|
ensure
|
|
rm_r @name
|
|
Process.wait(pid) if pid
|
|
end
|
|
end
|
|
end
|
|
|
|
it_should_behave_like :process_spawn_does_not_close_std_streams
|
|
end
|
|
|
|
context "when passed close_others: false" do
|
|
before :each do
|
|
@options = { close_others: false }
|
|
end
|
|
|
|
it "closes file descriptors >= 3 in the child process because they are set close_on_exec by default" do
|
|
touch @name
|
|
IO.pipe do |r, w|
|
|
begin
|
|
pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options)
|
|
w.close
|
|
r.read(1).should == nil
|
|
ensure
|
|
rm_r @name
|
|
Process.wait(pid) if pid
|
|
end
|
|
end
|
|
end
|
|
|
|
it "does not close file descriptors >= 3 in the child process if fds are set close_on_exec=false" do
|
|
IO.pipe do |r, w|
|
|
r.close_on_exec = false
|
|
w.close_on_exec = false
|
|
|
|
code = "fd = IO.for_fd(#{w.fileno}); fd.autoclose = false; fd.write 'abc'; fd.close"
|
|
pid = Process.spawn(ruby_cmd(code), @options)
|
|
begin
|
|
w.close
|
|
r.read.should == 'abc'
|
|
ensure
|
|
Process.wait(pid)
|
|
end
|
|
end
|
|
end
|
|
|
|
it_should_behave_like :process_spawn_does_not_close_std_streams
|
|
end
|
|
end
|
|
|
|
# error handling
|
|
|
|
it "raises an ArgumentError if passed no command arguments" do
|
|
-> { Process.spawn }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError if passed env or options but no command arguments" do
|
|
-> { Process.spawn({}) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError if passed env and options but no command arguments" do
|
|
-> { Process.spawn({}, {}) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an Errno::ENOENT for an empty string" do
|
|
-> { Process.spawn "" }.should raise_error(Errno::ENOENT)
|
|
end
|
|
|
|
it "raises an Errno::ENOENT if the command does not exist" do
|
|
-> { Process.spawn "nonesuch" }.should raise_error(Errno::ENOENT)
|
|
end
|
|
|
|
unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable
|
|
platform_is_not :windows do
|
|
it "raises an Errno::EACCES when the file does not have execute permissions" do
|
|
-> { Process.spawn __FILE__ }.should raise_error(Errno::EACCES)
|
|
end
|
|
end
|
|
|
|
platform_is :windows do
|
|
it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do
|
|
-> { Process.spawn __FILE__ }.should raise_error(SystemCallError) { |e|
|
|
[Errno::EACCES, Errno::ENOEXEC].should include(e.class)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
it "raises an Errno::EACCES or Errno::EISDIR when passed a directory" do
|
|
-> { Process.spawn File.dirname(__FILE__) }.should raise_error(SystemCallError) { |e|
|
|
[Errno::EACCES, Errno::EISDIR].should include(e.class)
|
|
}
|
|
end
|
|
|
|
it "raises an ArgumentError when passed a string key in options" do
|
|
-> { Process.spawn("echo", "chdir" => Dir.pwd) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an ArgumentError when passed an unknown option key" do
|
|
-> { Process.spawn("echo", nonesuch: :foo) }.should raise_error(ArgumentError)
|
|
end
|
|
|
|
platform_is_not :windows, :aix do
|
|
describe "with Integer option keys" do
|
|
before :each do
|
|
@name = tmp("spawn_fd_map.txt")
|
|
@io = new_io @name, "w+"
|
|
@io.sync = true
|
|
end
|
|
|
|
after :each do
|
|
@io.close unless @io.closed?
|
|
rm_r @name
|
|
end
|
|
|
|
it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do
|
|
child_fd = find_unused_fd
|
|
args = ruby_cmd(fixture(__FILE__, "map_fd.rb"), args: [child_fd.to_s])
|
|
pid = Process.spawn(*args, { child_fd => @io })
|
|
Process.waitpid pid
|
|
@io.rewind
|
|
|
|
@io.read.should == "writing to fd: #{child_fd}"
|
|
end
|
|
end
|
|
end
|
|
end
|