# frozen_string_literal: false require 'test/unit' require 'timeout' require 'tempfile' class TestSignal < Test::Unit::TestCase def test_signal begin x = 0 oldtrap = Signal.trap(:INT) {|sig| x = 2 } Process.kill :INT, Process.pid 10.times do break if 2 == x sleep 0.1 end assert_equal 2, x Signal.trap(:INT) { raise "Interrupt" } assert_raise_with_message(RuntimeError, /Interrupt/) { Process.kill :INT, Process.pid sleep 0.1 } ensure Signal.trap :INT, oldtrap if oldtrap end end if Process.respond_to?(:kill) def test_signal_process_group bug4362 = '[ruby-dev:43169]' assert_nothing_raised(bug4362) do cmd = [ EnvUtil.rubybin, '--disable=gems' '-e', 'sleep 10' ] pid = Process.spawn(*cmd, :pgroup => true) Process.kill(:"-TERM", pid) Process.waitpid(pid) assert_equal(true, $?.signaled?) assert_equal(Signal.list["TERM"], $?.termsig) end end if Process.respond_to?(:kill) and Process.respond_to?(:pgroup) # for mswin32 def test_exit_action if Signal.list[sig = "USR1"] term = :TERM else sig = "INT" term = :KILL end IO.popen([EnvUtil.rubybin, '--disable=gems', '-e', <<-"End"], 'r+') do |io| Signal.trap(:#{sig}, "EXIT") STDOUT.syswrite("a") Thread.start { sleep(2) } STDIN.sysread(4096) End pid = io.pid io.sysread(1) sleep 0.1 assert_nothing_raised("[ruby-dev:26128]") { Process.kill(term, pid) begin Timeout.timeout(3) { Process.waitpid pid } rescue Timeout::Error if term Process.kill(term, pid) term = (:KILL if term != :KILL) retry end raise end } end end if Process.respond_to?(:kill) def test_invalid_signal_name assert_raise(ArgumentError) { Process.kill(:XXXXXXXXXX, $$) } assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Process.kill("\u{30eb 30d3 30fc}", $$) } end if Process.respond_to?(:kill) def test_signal_exception assert_raise(ArgumentError) { SignalException.new } assert_raise(ArgumentError) { SignalException.new(-1) } assert_raise(ArgumentError) { SignalException.new(:XXXXXXXXXX) } assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { SignalException.new("\u{30eb 30d3 30fc}") } Signal.list.each do |signm, signo| next if signm == "EXIT" assert_equal(signo, SignalException.new(signm).signo, signm) assert_equal(signo, SignalException.new(signm.to_sym).signo, signm) assert_equal(signo, SignalException.new(signo).signo, signo) end e = assert_raise(ArgumentError) {SignalException.new("-SIGEXIT")} assert_not_match(/SIG-SIG/, e.message) end def test_interrupt assert_raise(Interrupt) { raise Interrupt.new } end def test_signal2 begin x = false oldtrap = Signal.trap(:INT) {|sig| x = true } GC.start assert_raise(ArgumentError) { Process.kill } Timeout.timeout(10) do x = false Process.kill(SignalException.new(:INT).signo, $$) sleep(0.01) until x x = false Process.kill("INT", $$) sleep(0.01) until x x = false Process.kill("SIGINT", $$) sleep(0.01) until x x = false o = Object.new def o.to_str; "SIGINT"; end Process.kill(o, $$) sleep(0.01) until x end assert_raise(ArgumentError) { Process.kill(Object.new, $$) } ensure Signal.trap(:INT, oldtrap) if oldtrap end end if Process.respond_to?(:kill) def test_trap begin oldtrap = Signal.trap(:INT) {|sig| } assert_raise(ArgumentError) { Signal.trap } # FIXME! Signal.trap(:INT, nil) Signal.trap(:INT, "") Signal.trap(:INT, "SIG_IGN") Signal.trap(:INT, "IGNORE") Signal.trap(:INT, "SIG_DFL") Signal.trap(:INT, "SYSTEM_DEFAULT") Signal.trap(:INT, "EXIT") Signal.trap(:INT, "xxxxxx") Signal.trap(:INT, "xxxx") Signal.trap(SignalException.new(:INT).signo, "SIG_DFL") assert_raise(ArgumentError) { Signal.trap(-1, "xxxx") } o = Object.new def o.to_str; "SIGINT"; end Signal.trap(o, "SIG_DFL") assert_raise(ArgumentError) { Signal.trap("XXXXXXXXXX", "SIG_DFL") } assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) { Signal.trap("\u{30eb 30d3 30fc}", "SIG_DFL") } assert_raise(ArgumentError) { Signal.trap("EXIT\0") {} } ensure Signal.trap(:INT, oldtrap) if oldtrap end end if Process.respond_to?(:kill) %w"KILL STOP".each do |sig| if Signal.list.key?(sig) define_method("test_trap_uncatchable_#{sig}") do assert_raise(Errno::EINVAL, "SIG#{sig} is not allowed to be caught") { Signal.trap(sig) {} } end end end def test_sigexit assert_in_out_err([], 'Signal.trap(:EXIT) {print "OK"}', ["OK"]) assert_in_out_err([], 'Signal.trap("EXIT") {print "OK"}', ["OK"]) assert_in_out_err([], 'Signal.trap(:SIGEXIT) {print "OK"}', ["OK"]) assert_in_out_err([], 'Signal.trap("SIGEXIT") {print "OK"}', ["OK"]) assert_in_out_err([], 'Signal.trap(0) {print "OK"}', ["OK"]) end def test_kill_immediately_before_termination Signal.list[sig = "USR1"] or sig = "INT" assert_in_out_err(["-e", <<-"end;"], "", %w"foo") Signal.trap(:#{sig}) { STDOUT.syswrite("foo") } Process.kill :#{sig}, $$ end; end if Process.respond_to?(:kill) def test_trap_system_default assert_separately([], <<-End) trap(:QUIT, "SYSTEM_DEFAULT") assert_equal("SYSTEM_DEFAULT", trap(:QUIT, "DEFAULT")) End end if Signal.list.key?('QUIT') def test_reserved_signal assert_raise(ArgumentError) { Signal.trap(:SEGV) {} } assert_raise(ArgumentError) { Signal.trap(:BUS) {} } assert_raise(ArgumentError) { Signal.trap(:ILL) {} } assert_raise(ArgumentError) { Signal.trap(:FPE) {} } assert_raise(ArgumentError) { Signal.trap(:VTALRM) {} } end def test_signame Signal.list.each do |name, num| assert_equal(num, Signal.list[Signal.signame(num)], name) end assert_nil(Signal.signame(-1)) signums = Signal.list.invert assert_nil(Signal.signame((1..1000).find {|num| !signums[num]})) end def test_signame_delivered args = [EnvUtil.rubybin, "--disable=gems", "-e", <<"", :err => File::NULL] Signal.trap("INT") do |signo| signame = Signal.signame(signo) Marshal.dump(signame, STDOUT) STDOUT.flush exit 0 end Process.kill("INT", $$) sleep 1 # wait signal deliver 10.times do IO.popen(args) do |child| signame = Marshal.load(child) assert_equal("INT", signame) end end end if Process.respond_to?(:kill) def test_trap_puts assert_in_out_err([], <<-INPUT, ["a"*10000], []) Signal.trap(:INT) { # for enable internal io mutex STDOUT.sync = false # larger than internal io buffer print "a"*10000 } Process.kill :INT, $$ sleep 0.1 INPUT end if Process.respond_to?(:kill) def test_hup_me # [Bug #7951] [ruby-core:52864] # This is MRI specific spec. ruby has no guarantee # that signal will be deliverd synchronously. # This ugly workaround was introduced to don't break # compatibility against silly example codes. assert_separately([], <<-RUBY) trap(:HUP, "DEFAULT") assert_raise(SignalException) { Process.kill('HUP', Process.pid) } RUBY bug8137 = '[ruby-dev:47182] [Bug #8137]' assert_nothing_raised(bug8137) { Timeout.timeout(1) { Process.kill(0, Process.pid) } } end if Process.respond_to?(:kill) and Signal.list.key?('HUP') def test_ignored_interrupt bug9820 = '[ruby-dev:48203] [Bug #9820]' assert_separately(['-', bug9820], <<-'end;') # begin bug = ARGV.shift trap(:INT, "IGNORE") assert_nothing_raised(SignalException, bug) do Process.kill(:INT, $$) end end; if trap = Signal.list['TRAP'] bug9820 = '[ruby-dev:48592] [Bug #9820]' status = assert_in_out_err(['-e', 'Process.kill(:TRAP, $$)']) assert_predicate(status, :signaled?, bug9820) assert_equal(trap, status.termsig, bug9820) end if Signal.list['CONT'] bug9820 = '[ruby-dev:48606] [Bug #9820]' assert_ruby_status(['-e', 'Process.kill(:CONT, $$)']) end end if Process.respond_to?(:kill) def test_signal_list_dedupe_keys a = Signal.list.keys.map(&:object_id).sort b = Signal.list.keys.map(&:object_id).sort assert_equal a, b end def test_self_stop assert_ruby_status([], <<-'end;') begin fork{ sleep 1 Process.kill(:CONT, Process.ppid) } Process.kill(:STOP, Process.pid) rescue NotImplementedError # ok end end; end def test_sigchld_ignore skip 'no SIGCHLD' unless Signal.list['CHLD'] old = trap(:CHLD, 'IGNORE') cmd = [ EnvUtil.rubybin, '--disable=gems', '-e' ] assert(system(*cmd, 'exit!(0)'), 'no ECHILD') IO.pipe do |r, w| pid = spawn(*cmd, "STDIN.read", in: r) nb = Process.wait(pid, Process::WNOHANG) th = Thread.new(Thread.current) do |parent| Thread.pass until parent.stop? # wait for parent to Process.wait w.close end assert_raise(Errno::ECHILD) { Process.wait(pid) } th.join assert_nil nb end IO.pipe do |r, w| pids = 3.times.map { spawn(*cmd, 'exit!', out: w) } w.close zombies = pids.dup assert_nil r.read(1), 'children dead' Timeout.timeout(10) do zombies.delete_if do |pid| begin Process.kill(0, pid) false rescue Errno::ESRCH true end end while zombies[0] end assert_predicate zombies, :empty?, 'zombies leftover' pids.each do |pid| assert_raise(Errno::ECHILD) { Process.waitpid(pid) } end end ensure trap(:CHLD, old) if Signal.list['CHLD'] end def test_sigwait_fd_unused t = EnvUtil.apply_timeout_scale(0.1) assert_separately([], <<-End) tgt = $$ trap(:TERM) { exit(0) } e = "Process.daemon; sleep #{t * 2}; Process.kill(:TERM,\#{tgt})" term = [ '#{EnvUtil.rubybin}', '--disable=gems', '-e', e ] t2 = Thread.new { sleep } # grab sigwait_fd Thread.pass until t2.stop? Thread.new do sleep #{t} t2.kill t2.join end Process.spawn(*term) # last thread remaining, ensure it can react to SIGTERM loop { sleep } End end if Process.respond_to?(:kill) && Process.respond_to?(:daemon) end