如果捕获到 SystemExit 异常,如何退出 strg-c 上的 ruby 程序

How can I exit a ruby program on strg-c if a SystemExit exception is being catched

我无法使用 strg-c (Ctrl-C) 中断的代码:

orig_std_out = STDOUT.clone
orig_std_err = STDERR.clone
STDOUT.reopen('/dev/null', 'w')
STDERR.reopen('/dev/null', 'w')

name = cookbook_name(File.join(path, 'Metadata.rb'))
error = 0

begin
  ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit
  error = 1
end
.
.
.

根据我的理解,如果我拯救 Exception,这种行为是合理的,但在这种情况下,我基本上是在捕获仅共享其父异常的兄弟姐妹 Exception

我已经尝试显式地挽救异常 InterruptSignalException

EDIT1:为了澄清我的问题,我添加了以下我尝试过的代码:

begin
  ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
  msg1 = e.message
  error = 1
rescue Interrupt
  msg2 = "interrupted"
end

在这两种情况下 - SystemExitKnife.run 和 Ctrl-C 抛出 - e.message returns "exit"。这不仅意味着 Ctrl-C 抛出一个 SystemExit 而我期望它抛出一个中断,而且错误消息是相同的。 我想我对 ruby 在那里的工作方式有很大的误解,因为我对 ruby 不是很熟悉。

EDIT2:进一步的测试显示一些 Ctrl-C 中断被 rescue Interrupt 挽救了。命令 ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"]) 是否有可能需要大约 3-5 秒到 运行,创建某种响应 Ctrl-C 的子进程,但总是以 SystemExit 和关闭rescue Interrupt 仅当此子进程未 运行ning 时被中断才有效?如果是这种情况,我如何才能中断整个程序?

EDIT3:我最初想附加所有在调用 Knife.run 时被调用的方法,但是,这将是太多的 LoC,尽管我认为我的子命令被执行的猜测是正确的。可以找到厨师 gem 代码 here。因此,以下摘录只是我认为有问题的部分:

 rescue Exception => e
  raise if raise_exception || Chef::Config[:verbosity] == 2
  humanize_exception(e)
  exit 100
end

这引出了一个问题:如何捕捉已经被子命令拯救的 Ctrl-C?

下面的代码展示了我是如何中断的:

interrupted = false
trap("INT") { interrupted = true} #sent INT to force exit in Knife.run and then exit

begin
  ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"]) #exits on error and on interrupt with 100
  if interrupted
    exit
  end
rescue SystemExit => e
  if interrupted
    exit
  end
  error = 1
end

缺点仍然是,我不能完全中断 Knife.run,而只能捕获中断并在该命令后检查是否触发了中断。我找不到同时捕获中断和 "reraise" 的方法,因此我至少能够从 Knife.run 中强制 exit 然后我可以手动退出。

我完成了gem install chef。现在我尝试另一种解决方案,仅替换 run_with_pretty_exceptions,但不知道将哪个 require 放入脚本中。我这样做了:

require 'chef'
$:.unshift('Users/b/.rvm/gems/ruby-2.3.3/gems/chef-13-6-4/lib')
require 'chef/knife'

但是然后:

$ ruby chef_knife.rb 
WARNING: No knife configuration file found
ERROR: Error connecting to https://supermarket.chef.io/api/v1/cookbooks/xyz, retry 1/5
...

因此,如果没有整个基础架构,我无法测试以下解决方案。这个想法是,在 Ruby 中,您可以重新打开现有的 class 并替换其他地方定义的方法。我必须离开你检查它:

# necessary require of chef and knife ...

class Chef::Knife # reopen the Knife class and replace this method
    def run_with_pretty_exceptions(raise_exception = false)
      unless respond_to?(:run)
        ui.error "You need to add a #run method to your knife command before you can use it"
      end
      enforce_path_sanity
      maybe_setup_fips
      Chef::LocalMode.with_server_connectivity do
        run
      end
    rescue Exception => e
      raise if e.class == Interrupt # <---------- added ********************
      raise if raise_exception || Chef::Config[:verbosity] == 2
      humanize_exception(e)
      exit 100
    end
end

name = cookbook_name(File.join(path, 'Metadata.rb'))
error = 0

begin
  ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
  puts "in rescue SystemExit e=#{e.inspect}"
  error = 1
rescue Interrupt
  puts 'in rescue Interrupt'
end

raise if e.class == Interrupt 如果是 1,将再次加注 Interrupt

通常我 运行 ruby -w 显示诊断信息,就像这样:

$ ruby -w ck.rb 
ck.rb:9: warning: method redefined; discarding old run_with_pretty_exceptions
ck.rb:4: warning: previous definition of run_with_pretty_exceptions was here

不幸的是,此 gem 中有太多未初始化的变量和循环要求警告,该选项会产生无法管理的输​​出。

此解决方案的缺点是您必须跟踪此更改的文档,如果 Chef 的版本发生更改,则必须有人验证 run_with_pretty_exceptions 的代码是否已更改。

请给我反馈。


=====更新=====

有一个侵入性较小的解决方案,它包括在 Chef::Knife.

中定义一个 exit 方法

当你看到exit 100,即没有接收者的消息,隐含的接收者是self,相当于self.exit 100。在我们的例子中,self 是由 instance = subcommand_class.new(args) 创建的对象,它是 instance.run_with_pretty_exceptions.

中的接收者

当消息发送到对象时,消息搜索机制开始查找该对象的 class。如果在 class 中没有这个名称的方法,搜索机制会查找包含的模块,然后是 superclass,等等,直到它到达 Object,[= 的默认 superclass 28=]。在这里它找到 Object#exit 并执行它。

Chef::Knife中定义了一个exit方法后,消息搜索机制在遇到exit 100Chef::Knife的实例作为隐式接收者时,会首先查找这个本地方法并执行它。通过先前为原始 Object#exit 添加别名,仍然可以调用原始 Ruby 方法来启动 Ruby 脚本的终止。这样,本地 exit 方法可以决定是调用原始 Object#exit 还是采取其他操作。

下面是一个完整的示例,演示了它是如何工作的。

# ***** Emulation of the gem *****

class Chef end

class Chef::Knife
    def self.run(x)
        puts 'in original run'
        self.new.run_with_pretty_exceptions
    end

    def run_with_pretty_exceptions
        print 'Press Ctrl_C > '
        gets
        rescue Exception => e
            puts
            puts "in run_with_pretty...'s Exception e=#{e.inspect} #{e.class}"
            raise if false # if raise_exception || Chef::Config[:verbosity] == 2
#            humanize_exception(e)
            puts "now $!=#{$!.inspect}"
            puts "about to exit,                     self=#{self}"
            exit 100
    end
end

# ***** End of gem emulation *****

#----------------------------------------------------------------------

# ***** This is what you put into your script. *****

class Chef::Knife # reopen the Knife class and define one's own exit
    alias_method :object_exit, :exit

    def exit(p)
        puts "in my own exit with parameter #{p}, self=#{self}"
        puts "$!=#{$!.inspect}"

        if Interrupt === $!
            puts 'then about to raise Interrupt'
            raise # re-raise Interrupt
        else
            puts 'else about to call Object#exit'
            object_exit(p)
        end
    end
end

begin
  ::Chef::Knife.run([])
rescue SystemExit => e
  puts "in script's rescue SystemExit e=#{e.inspect}"
rescue Interrupt
  puts "in script's rescue Interrupt"
end

执行。首先用 Ctrl-C 测试:

$ ruby -w simul_chef.rb 
in original run
Press Ctrl_C > ^C
in run_with_pretty...'s Exception e=Interrupt Interrupt
now $!=Interrupt
about to exit,                     self=#<Chef::Knife:0x007fb2361c7038>
in my own exit with parameter 100, self=#<Chef::Knife:0x007fb2361c7038>
$!=Interrupt
then about to raise Interrupt
in script's rescue Interrupt

第二次硬中断测试。

在一个终端中 window :

$ ruby -w simul_chef.rb 
in original run
Press Ctrl_C > 

在另一个终端中 window :

$ ps -ef
  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0 Fri01PM ??         0:52.65 /sbin/launchd
...
    0   363   282   0 Fri01PM ttys000    0:00.02 login -pfl b /bin/bash -c exec -la bash /bin/bash
  501   364   363   0 Fri01PM ttys000    0:00.95 -bash
  501  3175   364   0  9:51PM ttys000    0:00.06 ruby -w simul_chef.rb
...
$ kill 3175

回到第一个航站楼:

in run_with_pretty...'s Exception e=#<SignalException: SIGTERM> SignalException
now $!=#<SignalException: SIGTERM>
about to exit,                     self=#<Chef::Knife:0x007fc5a79d70a0>
in my own exit with parameter 100, self=#<Chef::Knife:0x007fc5a79d70a0>
$!=#<SignalException: SIGTERM>
else about to call Object#exit
in script's rescue SystemExit e=#<SystemExit: exit>

考虑到您最初发布的代码,您所要做的就是在开头插入,但在必要的 require 之后:

class Chef::Knife # reopen the Knife class and define one's own exit
    alias_method :object_exit, :exit

    def exit(p)
        if Interrupt === $!
            raise # re-raise Interrupt
        else
            object_exit(p)
        end
    end
end

所以没必要动原文gem.