如果捕获到 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
。
我已经尝试显式地挽救异常 Interrupt
和 SignalException
。
EDIT1:为了澄清我的问题,我添加了以下我尝试过的代码:
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
msg1 = e.message
error = 1
rescue Interrupt
msg2 = "interrupted"
end
在这两种情况下 - SystemExit
由 Knife.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 100
以Chef::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.
我无法使用 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
。
我已经尝试显式地挽救异常 Interrupt
和 SignalException
。
EDIT1:为了澄清我的问题,我添加了以下我尝试过的代码:
begin
::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"])
rescue SystemExit => e
msg1 = e.message
error = 1
rescue Interrupt
msg2 = "interrupted"
end
在这两种情况下 - SystemExit
由 Knife.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 100
以Chef::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.