1require_relative '../../spec_helper' 2require_relative 'fixtures/common' 3 4newline = "\n" 5platform_is :windows do 6 newline = "\r\n" 7end 8 9describe :process_spawn_does_not_close_std_streams, shared: true do 10 platform_is_not :windows do 11 it "does not close STDIN" do 12 code = "STDOUT.puts STDIN.read(0).inspect" 13 cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" 14 ruby_exe(cmd, args: "> #{@output}") 15 File.binread(@output).should == %[""#{newline}] 16 end 17 18 it "does not close STDOUT" do 19 code = "STDOUT.puts 'hello'" 20 cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" 21 ruby_exe(cmd, args: "> #{@output}") 22 File.binread(@output).should == "hello#{newline}" 23 end 24 25 it "does not close STDERR" do 26 code = "STDERR.puts 'hello'" 27 cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" 28 ruby_exe(cmd, args: "2> #{@output}") 29 File.binread(@output).should =~ /hello#{newline}/ 30 end 31 end 32end 33 34describe "Process.spawn" do 35 ProcessSpecs.use_system_ruby(self) 36 37 before :each do 38 @name = tmp("process_spawn.txt") 39 @var = "$FOO" 40 platform_is :windows do 41 @var = "%FOO%" 42 end 43 end 44 45 after :each do 46 rm_r @name 47 end 48 49 it "executes the given command" do 50 lambda { Process.wait Process.spawn("echo spawn") }.should output_to_fd("spawn\n") 51 end 52 53 it "returns the process ID of the new process as a Fixnum" do 54 pid = Process.spawn(*ruby_exe, "-e", "exit") 55 Process.wait pid 56 pid.should be_an_instance_of(Fixnum) 57 end 58 59 it "returns immediately" do 60 start = Time.now 61 pid = Process.spawn(*ruby_exe, "-e", "sleep 10") 62 (Time.now - start).should < 5 63 Process.kill :KILL, pid 64 Process.wait pid 65 end 66 67 # argv processing 68 69 describe "with a single argument" do 70 platform_is_not :windows do 71 it "subjects the specified command to shell expansion" do 72 lambda { Process.wait Process.spawn("echo *") }.should_not output_to_fd("*\n") 73 end 74 75 it "creates an argument array with shell parsing semantics for whitespace" do 76 lambda { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n") 77 end 78 end 79 80 platform_is :windows do 81 # There is no shell expansion on Windows 82 it "does not subject the specified command to shell expansion on Windows" do 83 lambda { Process.wait Process.spawn("echo *") }.should output_to_fd("*\n") 84 end 85 86 it "does not create an argument array with shell parsing semantics for whitespace on Windows" do 87 lambda { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n") 88 end 89 end 90 91 it "calls #to_str to convert the argument to a String" do 92 o = mock("to_str") 93 o.should_receive(:to_str).and_return("echo foo") 94 lambda { Process.wait Process.spawn(o) }.should output_to_fd("foo\n") 95 end 96 97 it "raises an ArgumentError if the command includes a null byte" do 98 lambda { Process.spawn "\000" }.should raise_error(ArgumentError) 99 end 100 101 it "raises a TypeError if the argument does not respond to #to_str" do 102 lambda { Process.spawn :echo }.should raise_error(TypeError) 103 end 104 end 105 106 describe "with multiple arguments" do 107 it "does not subject the arguments to shell expansion" do 108 lambda { Process.wait Process.spawn("echo", "*") }.should output_to_fd("*\n") 109 end 110 111 it "preserves whitespace in passed arguments" do 112 out = "a b c d\n" 113 platform_is :windows do 114 # The echo command on Windows takes quotes literally 115 out = "\"a b c d\"\n" 116 end 117 lambda { Process.wait Process.spawn("echo", "a b c d") }.should output_to_fd(out) 118 end 119 120 it "calls #to_str to convert the arguments to Strings" do 121 o = mock("to_str") 122 o.should_receive(:to_str).and_return("foo") 123 lambda { Process.wait Process.spawn("echo", o) }.should output_to_fd("foo\n") 124 end 125 126 it "raises an ArgumentError if an argument includes a null byte" do 127 lambda { Process.spawn "echo", "\000" }.should raise_error(ArgumentError) 128 end 129 130 it "raises a TypeError if an argument does not respond to #to_str" do 131 lambda { Process.spawn "echo", :foo }.should raise_error(TypeError) 132 end 133 end 134 135 describe "with a command array" do 136 it "uses the first element as the command name and the second as the argv[0] value" do 137 platform_is_not :windows do 138 lambda { Process.wait Process.spawn(["/bin/sh", "argv_zero"], "-c", "echo $0") }.should output_to_fd("argv_zero\n") 139 end 140 platform_is :windows do 141 lambda { Process.wait Process.spawn(["cmd.exe", "/C"], "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n") 142 end 143 end 144 145 it "does not subject the arguments to shell expansion" do 146 lambda { Process.wait Process.spawn(["echo", "echo"], "*") }.should output_to_fd("*\n") 147 end 148 149 it "preserves whitespace in passed arguments" do 150 out = "a b c d\n" 151 platform_is :windows do 152 # The echo command on Windows takes quotes literally 153 out = "\"a b c d\"\n" 154 end 155 lambda { Process.wait Process.spawn(["echo", "echo"], "a b c d") }.should output_to_fd(out) 156 end 157 158 it "calls #to_ary to convert the argument to an Array" do 159 o = mock("to_ary") 160 platform_is_not :windows do 161 o.should_receive(:to_ary).and_return(["/bin/sh", "argv_zero"]) 162 lambda { Process.wait Process.spawn(o, "-c", "echo $0") }.should output_to_fd("argv_zero\n") 163 end 164 platform_is :windows do 165 o.should_receive(:to_ary).and_return(["cmd.exe", "/C"]) 166 lambda { Process.wait Process.spawn(o, "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n") 167 end 168 end 169 170 it "calls #to_str to convert the first element to a String" do 171 o = mock("to_str") 172 o.should_receive(:to_str).and_return("echo") 173 lambda { Process.wait Process.spawn([o, "echo"], "foo") }.should output_to_fd("foo\n") 174 end 175 176 it "calls #to_str to convert the second element to a String" do 177 o = mock("to_str") 178 o.should_receive(:to_str).and_return("echo") 179 lambda { Process.wait Process.spawn(["echo", o], "foo") }.should output_to_fd("foo\n") 180 end 181 182 it "raises an ArgumentError if the Array does not have exactly two elements" do 183 lambda { Process.spawn([]) }.should raise_error(ArgumentError) 184 lambda { Process.spawn([:a]) }.should raise_error(ArgumentError) 185 lambda { Process.spawn([:a, :b, :c]) }.should raise_error(ArgumentError) 186 end 187 188 it "raises an ArgumentError if the Strings in the Array include a null byte" do 189 lambda { Process.spawn ["\000", "echo"] }.should raise_error(ArgumentError) 190 lambda { Process.spawn ["echo", "\000"] }.should raise_error(ArgumentError) 191 end 192 193 it "raises a TypeError if an element in the Array does not respond to #to_str" do 194 lambda { Process.spawn ["echo", :echo] }.should raise_error(TypeError) 195 lambda { Process.spawn [:echo, "echo"] }.should raise_error(TypeError) 196 end 197 end 198 199 # env handling 200 201 after :each do 202 ENV.delete("FOO") 203 end 204 205 it "sets environment variables in the child environment" do 206 Process.wait Process.spawn({"FOO" => "BAR"}, "echo #{@var}>#{@name}") 207 File.read(@name).should == "BAR\n" 208 end 209 210 it "unsets environment variables whose value is nil" do 211 ENV["FOO"] = "BAR" 212 Process.wait Process.spawn({"FOO" => nil}, "echo #{@var}>#{@name}") 213 expected = "\n" 214 platform_is :windows do 215 # Windows does not expand the variable if it is unset 216 expected = "#{@var}\n" 217 end 218 File.read(@name).should == expected 219 end 220 221 it "calls #to_hash to convert the environment" do 222 o = mock("to_hash") 223 o.should_receive(:to_hash).and_return({"FOO" => "BAR"}) 224 Process.wait Process.spawn(o, "echo #{@var}>#{@name}") 225 File.read(@name).should == "BAR\n" 226 end 227 228 it "calls #to_str to convert the environment keys" do 229 o = mock("to_str") 230 o.should_receive(:to_str).and_return("FOO") 231 Process.wait Process.spawn({o => "BAR"}, "echo #{@var}>#{@name}") 232 File.read(@name).should == "BAR\n" 233 end 234 235 it "calls #to_str to convert the environment values" do 236 o = mock("to_str") 237 o.should_receive(:to_str).and_return("BAR") 238 Process.wait Process.spawn({"FOO" => o}, "echo #{@var}>#{@name}") 239 File.read(@name).should == "BAR\n" 240 end 241 242 it "raises an ArgumentError if an environment key includes an equals sign" do 243 lambda do 244 Process.spawn({"FOO=" => "BAR"}, "echo #{@var}>#{@name}") 245 end.should raise_error(ArgumentError) 246 end 247 248 it "raises an ArgumentError if an environment key includes a null byte" do 249 lambda do 250 Process.spawn({"\000" => "BAR"}, "echo #{@var}>#{@name}") 251 end.should raise_error(ArgumentError) 252 end 253 254 it "raises an ArgumentError if an environment value includes a null byte" do 255 lambda do 256 Process.spawn({"FOO" => "\000"}, "echo #{@var}>#{@name}") 257 end.should raise_error(ArgumentError) 258 end 259 260 # :unsetenv_others 261 262 before :each do 263 @minimal_env = { 264 "PATH" => ENV["PATH"], 265 "HOME" => ENV["HOME"] 266 } 267 @common_env_spawn_args = [@minimal_env, "echo #{@var}>#{@name}"] 268 end 269 270 platform_is_not :windows do 271 it "unsets other environment variables when given a true :unsetenv_others option" do 272 ENV["FOO"] = "BAR" 273 Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: true) 274 $?.success?.should be_true 275 File.read(@name).should == "\n" 276 end 277 end 278 279 it "does not unset other environment variables when given a false :unsetenv_others option" do 280 ENV["FOO"] = "BAR" 281 Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: false) 282 $?.success?.should be_true 283 File.read(@name).should == "BAR\n" 284 end 285 286 platform_is_not :windows do 287 it "does not unset environment variables included in the environment hash" do 288 env = @minimal_env.merge({"FOO" => "BAR"}) 289 Process.wait Process.spawn(env, "echo #{@var}>#{@name}", unsetenv_others: true) 290 $?.success?.should be_true 291 File.read(@name).should == "BAR\n" 292 end 293 end 294 295 # :pgroup 296 297 platform_is_not :windows do 298 it "joins the current process group by default" do 299 lambda do 300 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)")) 301 end.should output_to_fd(Process.getpgid(Process.pid).to_s) 302 end 303 304 it "joins the current process if pgroup: false" do 305 lambda do 306 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: false) 307 end.should output_to_fd(Process.getpgid(Process.pid).to_s) 308 end 309 310 it "joins the current process if pgroup: nil" do 311 lambda do 312 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: nil) 313 end.should output_to_fd(Process.getpgid(Process.pid).to_s) 314 end 315 316 it "joins a new process group if pgroup: true" do 317 process = lambda do 318 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: true) 319 end 320 321 process.should_not output_to_fd(Process.getpgid(Process.pid).to_s) 322 process.should output_to_fd(/\d+/) 323 end 324 325 it "joins a new process group if pgroup: 0" do 326 process = lambda do 327 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: 0) 328 end 329 330 process.should_not output_to_fd(Process.getpgid(Process.pid).to_s) 331 process.should output_to_fd(/\d+/) 332 end 333 334 it "joins the specified process group if pgroup: pgid" do 335 pgid = Process.getpgid(Process.pid) 336 lambda do 337 Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: pgid) 338 end.should output_to_fd(pgid.to_s) 339 end 340 341 it "raises an ArgumentError if given a negative :pgroup option" do 342 lambda { Process.spawn("echo", pgroup: -1) }.should raise_error(ArgumentError) 343 end 344 345 it "raises a TypeError if given a symbol as :pgroup option" do 346 lambda { Process.spawn("echo", pgroup: :true) }.should raise_error(TypeError) 347 end 348 end 349 350 platform_is :windows do 351 it "raises an ArgumentError if given :pgroup option" do 352 lambda { Process.spawn("echo", pgroup: false) }.should raise_error(ArgumentError) 353 end 354 end 355 356 # :rlimit_core 357 # :rlimit_cpu 358 # :rlimit_data 359 360 # :chdir 361 362 it "uses the current working directory as its working directory" do 363 lambda do 364 Process.wait Process.spawn(ruby_cmd("print Dir.pwd")) 365 end.should output_to_fd(Dir.pwd) 366 end 367 368 describe "when passed :chdir" do 369 before do 370 @dir = tmp("spawn_chdir", false) 371 Dir.mkdir @dir 372 end 373 374 after do 375 rm_r @dir 376 end 377 378 it "changes to the directory passed for :chdir" do 379 lambda do 380 Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: @dir) 381 end.should output_to_fd(@dir) 382 end 383 384 it "calls #to_path to convert the :chdir value" do 385 dir = mock("spawn_to_path") 386 dir.should_receive(:to_path).and_return(@dir) 387 388 lambda do 389 Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: dir) 390 end.should output_to_fd(@dir) 391 end 392 end 393 394 # :umask 395 396 it "uses the current umask by default" do 397 lambda do 398 Process.wait Process.spawn(ruby_cmd("print File.umask")) 399 end.should output_to_fd(File.umask.to_s) 400 end 401 402 platform_is_not :windows do 403 it "sets the umask if given the :umask option" do 404 lambda do 405 Process.wait Process.spawn(ruby_cmd("print File.umask"), umask: 146) 406 end.should output_to_fd("146") 407 end 408 end 409 410 # redirection 411 412 it "redirects STDOUT to the given file descriptior if out: Fixnum" do 413 File.open(@name, 'w') do |file| 414 lambda do 415 Process.wait Process.spawn("echo glark", out: file.fileno) 416 end.should output_to_fd("glark\n", file) 417 end 418 end 419 420 it "redirects STDOUT to the given file if out: IO" do 421 File.open(@name, 'w') do |file| 422 lambda do 423 Process.wait Process.spawn("echo glark", out: file) 424 end.should output_to_fd("glark\n", file) 425 end 426 end 427 428 it "redirects STDOUT to the given file if out: String" do 429 Process.wait Process.spawn("echo glark", out: @name) 430 File.read(@name).should == "glark\n" 431 end 432 433 it "redirects STDOUT to the given file if out: [String name, String mode]" do 434 Process.wait Process.spawn("echo glark", out: [@name, 'w']) 435 File.read(@name).should == "glark\n" 436 end 437 438 it "redirects STDERR to the given file descriptior if err: Fixnum" do 439 File.open(@name, 'w') do |file| 440 lambda do 441 Process.wait Process.spawn("echo glark>&2", err: file.fileno) 442 end.should output_to_fd("glark\n", file) 443 end 444 end 445 446 it "redirects STDERR to the given file descriptor if err: IO" do 447 File.open(@name, 'w') do |file| 448 lambda do 449 Process.wait Process.spawn("echo glark>&2", err: file) 450 end.should output_to_fd("glark\n", file) 451 end 452 end 453 454 it "redirects STDERR to the given file if err: String" do 455 Process.wait Process.spawn("echo glark>&2", err: @name) 456 File.read(@name).should == "glark\n" 457 end 458 459 it "redirects STDERR to child STDOUT if :err => [:child, :out]" do 460 File.open(@name, 'w') do |file| 461 lambda do 462 Process.wait Process.spawn("echo glark>&2", :out => file, :err => [:child, :out]) 463 end.should output_to_fd("glark\n", file) 464 end 465 end 466 467 it "redirects both STDERR and STDOUT to the given file descriptior" do 468 File.open(@name, 'w') do |file| 469 lambda do 470 Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), 471 [:out, :err] => file.fileno) 472 end.should output_to_fd("glarkbang", file) 473 end 474 end 475 476 it "redirects both STDERR and STDOUT to the given IO" do 477 File.open(@name, 'w') do |file| 478 lambda do 479 Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), 480 [:out, :err] => file) 481 end.should output_to_fd("glarkbang", file) 482 end 483 end 484 485 it "redirects both STDERR and STDOUT at the time to the given name" do 486 touch @name 487 Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), [:out, :err] => @name) 488 File.read(@name).should == "glarkbang" 489 end 490 491 context "when passed close_others: true" do 492 before :each do 493 @output = tmp("spawn_close_others_true") 494 @options = { close_others: true } 495 end 496 497 after :each do 498 rm_r @output 499 end 500 501 it "closes file descriptors >= 3 in the child process" do 502 IO.pipe do |r, w| 503 begin 504 pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options) 505 w.close 506 lambda { r.read_nonblock(1) }.should raise_error(EOFError) 507 ensure 508 rm_r @name 509 Process.wait(pid) if pid 510 end 511 end 512 end 513 514 it_should_behave_like :process_spawn_does_not_close_std_streams 515 end 516 517 context "when passed close_others: false" do 518 before :each do 519 @output = tmp("spawn_close_others_false") 520 @options = { close_others: false } 521 end 522 523 after :each do 524 rm_r @output 525 end 526 527 it "closes file descriptors >= 3 in the child process because they are set close_on_exec by default" do 528 IO.pipe do |r, w| 529 begin 530 pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options) 531 w.close 532 lambda { r.read_nonblock(1) }.should raise_error(EOFError) 533 ensure 534 rm_r @name 535 Process.wait(pid) if pid 536 end 537 end 538 end 539 540 platform_is_not :windows do 541 it "does not close file descriptors >= 3 in the child process if fds are set close_on_exec=false" do 542 IO.pipe do |r, w| 543 r.close_on_exec = false 544 w.close_on_exec = false 545 begin 546 pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options) 547 w.close 548 lambda { r.read_nonblock(1) }.should raise_error(Errno::EAGAIN) 549 ensure 550 rm_r @name 551 Process.wait(pid) if pid 552 end 553 end 554 end 555 end 556 557 it_should_behave_like :process_spawn_does_not_close_std_streams 558 end 559 560 # error handling 561 562 it "raises an ArgumentError if passed no command arguments" do 563 lambda { Process.spawn }.should raise_error(ArgumentError) 564 end 565 566 it "raises an ArgumentError if passed env or options but no command arguments" do 567 lambda { Process.spawn({}) }.should raise_error(ArgumentError) 568 end 569 570 it "raises an ArgumentError if passed env and options but no command arguments" do 571 lambda { Process.spawn({}, {}) }.should raise_error(ArgumentError) 572 end 573 574 it "raises an Errno::ENOENT for an empty string" do 575 lambda { Process.spawn "" }.should raise_error(Errno::ENOENT) 576 end 577 578 it "raises an Errno::ENOENT if the command does not exist" do 579 lambda { Process.spawn "nonesuch" }.should raise_error(Errno::ENOENT) 580 end 581 582 unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable 583 platform_is_not :windows do 584 it "raises an Errno::EACCES when the file does not have execute permissions" do 585 lambda { Process.spawn __FILE__ }.should raise_error(Errno::EACCES) 586 end 587 end 588 589 platform_is :windows do 590 it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do 591 lambda { Process.spawn __FILE__ }.should raise_error(SystemCallError) { |e| 592 [Errno::EACCES, Errno::ENOEXEC].should include(e.class) 593 } 594 end 595 end 596 end 597 598 it "raises an Errno::EACCES or Errno::EISDIR when passed a directory" do 599 lambda { Process.spawn File.dirname(__FILE__) }.should raise_error(SystemCallError) { |e| 600 [Errno::EACCES, Errno::EISDIR].should include(e.class) 601 } 602 end 603 604 it "raises an ArgumentError when passed a string key in options" do 605 lambda { Process.spawn("echo", "chdir" => Dir.pwd) }.should raise_error(ArgumentError) 606 end 607 608 it "raises an ArgumentError when passed an unknown option key" do 609 lambda { Process.spawn("echo", nonesuch: :foo) }.should raise_error(ArgumentError) 610 end 611 612 platform_is_not :windows do 613 describe "with Integer option keys" do 614 before :each do 615 @name = tmp("spawn_fd_map.txt") 616 @io = new_io @name, "w+" 617 @io.sync = true 618 end 619 620 after :each do 621 @io.close unless @io.closed? 622 rm_r @name 623 end 624 625 it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do 626 child_fd = @io.fileno + 1 627 args = ruby_cmd(fixture(__FILE__, "map_fd.rb"), args: [child_fd.to_s]) 628 pid = Process.spawn(*args, { child_fd => @io }) 629 Process.waitpid pid 630 @io.rewind 631 632 @io.read.should == "writing to fd: #{child_fd}" 633 end 634 end 635 end 636end 637