You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby嵌套进程中无法捕获INT/TERM信号的问题求助

如何阻止Ctrl+C中断传递至代理进程(同时运行Rails控制台+代理)

问题描述

我正在编写一个脚本,需实现两个功能:

  1. 启动代理
  2. 运行带有pry和自定义提示符的rails控制台

问题在于无法阻止Ctrl+C的中断信号传递至代理进程。我已尝试以下方案,但均无效:

  • 在执行流程的多个阶段捕获INT和TERM信号
  • 生成包含代理命令的bash脚本并运行
  • 运行独立脚本分别处理代理任务与控制台/pry代码

恳请各位提供解决方案。

测试脚本及运行要求

运行要求

  • 需安装pry
  • 脚本需放在Rails应用根目录
  • 执行chmod +x赋予可执行权限

测试脚本

#!/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

解决方案

核心问题:当你在终端按下Ctrl+C时,终端会向整个前台进程组发送INT信号,代理进程与控制台进程同属一个进程组,因此都会收到信号。要解决这个问题,需让代理进程脱离当前前台进程组。

方法1:用setsid让代理进程脱离终端控制

修改start_script方法,在exec命令前添加setsid,让代理进程成为新会话的首进程,彻底脱离原终端的信号传递:

def start_script
  @bash_job = fork do
    puts "=== 代理进程启动,已脱离终端进程组 ==="
    exec("setsid echo Starting proxy && sleep 5 && echo Ending proxy")
  end
  Process.detach(@bash_job)
end

setsid会创建新会话,使子进程脱离原终端,终端的Ctrl+C信号不会再传递给它。

方法2:手动设置独立进程组

在fork后的代理子进程中调用Process.setpgrp,创建新的进程组,让代理进程成为组长,脱离前台进程组:

def start_script
  @bash_job = fork do
    Process.setpgrp # 创建独立进程组
    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

此时终端的INT信号只会发送给前台进程组(控制台进程所在组),不会影响代理进程。

额外优化:处理Pry的信号拦截

Pry本身会默认处理INT信号(用于中断当前输入),如果你想在Pry中自定义Ctrl+C的行为,可以通过Pry钩子实现:

# 修改pry_code中的信号处理部分
Pry.hooks.add(:before_session, :override_int) do
  Signal.trap("INT") do
    puts "\nglobal INT. Run `!!!`, `quit`, or `exit` to terminate"
    Pry::InputLock.release # 释放输入锁,避免Pry卡住
  end
end

内容的提问来源于stack exchange,提问作者MGreenfield

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 08:43:16