Unable to trap INT/TERM signals in nested Ruby processes

222 views Asked by At

I'm trying to make a script that does 2 things: (1) starts a proxy and (2) runs a rails console with pry and a custom prompt

The problem is that I can't figure out how to prevent Ctrl+C interrupts from reaching the proxy process.

Here's what I've tried:

  • Trap INT and TERM signals at several different parts of execution
  • Generating a bash script with the proxy command and running that
  • Running a separate script that runs the proxy job and the console/pry code

Nothing has worked so far.

If you have any suggestions, they would be greatly appreciated.

Here's a test script to try it out. Here's what you'll need for it:

  • pry installed
  • The script in a rails app root folder
  • To run chmod +x on it so it's executable
#!/usr/bin/env ruby

class TestInterrupt
  include Process
  include Signal

  def start
    override_interrupt
    start_script
    start_console

    exit
  end

  private

  attr_reader :bash_job, :console_job

  def override_interrupt
    trap("INT")  { puts "global INT. Run `!!!`, `quit`, or `exit` to terminate"  }
    trap("TERM") { puts "global TERM. Run `!!!`, `quit`, or `exit` to terminate" }
    puts "=== Trapping global INT, TERM ==="
  end

  def start_script
    @bash_job = fork do
      trap("INT")  { puts "bash_job INT" }
      trap("TERM") { puts "bash_job TERM" }
      puts "=== Trapping bash_job INT, TERM ==="
      exec("echo Starting shell script && sleep 5 && echo Ending shell script")
    end
    Process.detach(@bash_job)
  end

  def start_console
    @console_job = fork do
      trap("INT")  { puts "console_job INT" }
      trap("TERM") { puts "console_job TERM" }
      puts "=== Trapping console INT, TERM ==="
      exec console_code
    end
    puts "=== Starting rails console (PID: console_job) ==="
    wait(console_job)
  end

  def console_code
    <<~BASH
      rails runner '#{pry_code}'
    BASH
  end

  def pry_code
    <<~RUBY
      require "pry"

      pry_prompt = Pry.config.prompt
      env_prompt = Pry::Helpers::Text.cyan("TestInterrupt")
      Pry.config.prompt = [
        proc { |*a| "\#{env_prompt} \#{pry_prompt.first.call(*a)}"  },
        proc { |*a| "\#{env_prompt} \#{pry_prompt.second.call(*a)}" },
      ]

      Signal.trap("INT") { puts "inner INT" }
      Signal.trap("TERM") { puts "inner TERM" }
      puts "=== Trapping inner INT, TERM ==="

      pry
    RUBY
  end
end

TestInterrupt.new.start
1

There are 1 answers

1
Alex On

I know INT is CTRL-C and CTRL-D is not a signal, so I can only debug this a little bit:

# its_a_trap.rb

begin
  trap("INT") { puts " INT" }
  sleep
ensure
  puts "exit"
end
$ ruby its_a_trap.rb
^C INT

Works, doesn't exit.


begin
  trap("INT") { puts " INT" }
  system "trap 'echo shell received INT signal' INT; sleep infinity"
ensure
  puts "exit"
end
$ ruby its_a_trap.rb
^C INT
shell received INT signal
exit

Doesn't work.


begin
  trap("INT", "IGNORE")
  system "trap 'echo shell received INT signal' INT; sleep infinity"
ensure
  puts "exit"
end
$ ruby its_a_trap.rb
^C^C^C^C^C

Works. Doesn't exit. Seeing is believing watch -n 1 ps ft pts/4:

2293186 pts/4    Ss     0:00 zsh
2350743 pts/4    S+     0:00  \_ ruby its_a_trap.rb
2350757 pts/4    S+     0:00      \_ sh -c trap 'echo shell received INT signal' INT; sleep infinity
2350758 pts/4    S+     0:00          \_ sleep infinity

Same thing with forks and execs and rails console:

fork do
  trap("INT") { puts "defunct" } # i don't know if it does anything here
  exec %(trap 'echo shell received INT signal' INT; sleep 20)
end

Process.detach(
  fork do
    trap("INT", "IGNORE") # this does something
    exec %(_='im sleeping'; sleep 20)
  end
)

trap("CLD") { puts "child process died" }
trap("INT") { puts "dont kill wait INT" }
Process.wait(
  # it was the same with pry, no difference
  fork { exec "rails console" }
)
$ ruby its_a_trap.rb
Loading development environment (Rails 7.0.5)
>> dont kill wait INT
^C
shell received INT signal
child process died
>>

One of them didn't make it:

2293186 pts/4    Ss     0:01 zsh
2359797 pts/4    Sl+    0:00  \_ ruby its_a_trap.rb
2359811 pts/4    Z+     0:00      \_ [sh] <defunct>
2359812 pts/4    S+     0:00      \_ sh -c _='im sleeping'; sleep 20
2359816 pts/4    S+     0:00      |   \_ sleep 20
2359814 pts/4    Sl+    0:01      \_ /home/alex/.rbenv/versions/3.2.2/bin/ruby bin/rails console