Ruby嵌套进程中无法捕获INT/TERM信号的问题求助
如何阻止Ctrl+C中断传递至代理进程(同时运行Rails控制台+代理)
问题描述
我正在编写一个脚本,需实现两个功能:
- 启动代理
- 运行带有
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
相关产品推荐
相关产品推荐

