Big X
Big X

Reputation: 98

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

The code which I can not interrupt by using 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
.
.
.

In my understanding this behaviour would be reasonable if I would rescue Exception, but in this case I am basically catching siblings which only share their parent exception Exception.

I have already tried to rescue the exception Interrupt and SignalException explicitly.

EDIT1: In the hope of clarifying my question I added the following code which i tried:

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

In both cases - SystemExit thrown by Knife.run and thrown by Ctrl-C - e.message returns "exit". This does not only mean, that Ctrl-C throws a SystemExit whereas I am expecting it to throw an Interrupt, but also that the error message is the same. I guess that I have got a major misunderstanding in how ruby works there, since I am not very familiar with ruby.

EDIT2: Further testing revealed that some Ctrl-C interrupts are rescued by rescue Interrupt. Is it possible that the command ::Chef::Knife.run(['cookbook', 'site', 'show', "#{name}"]), which takes about 3-5 seconds to run, creates some kind of subprocess which responds to a Ctrl-C, but always closes with a SystemExit and that rescue Interruptonly works when it is interrupted just the moment this subprocess is not running? If this is the case, how am I going to be able to Interrupt the whole program?

EDIT3: I initially wanted to attach all the methods which get called on calling Knife.run, however, this would have been too many LoC, although I think my guess that a subcommand is executed was right. The chef gem code can be found here. Thus, the following excerpt is only the part which is the problematic one in my opinion:

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

Which leads to the question: How can I catch a Ctrl-C which is already rescued by a subcommand?

Upvotes: 0

Views: 732

Answers (2)

BernardK
BernardK

Reputation: 3734

I have done gem install chef. Now I try another solution, replacing only run_with_pretty_exceptions, but don't know which require to put in the script. I did this :

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

But then :

$ 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
...

So, without the whole infrastructure, I can't test the following solution. The idea is that in Ruby you can reopen an existing class and replace a method defined elsewhere. I have to leave you check it :

# 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 will re-raise Interrupt if it is one.

Normally I run ruby -w to display diagnostics, which would be like this :

$ 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

Unfortunately there are so many uninitialized variables and circular require warnings in this gem that this option produces un unmanageable output.

The drawback of this solution is that you have to keep a documentation track of this change, and in case of Chef's release change, somebody has to verify if the code of run_with_pretty_exceptions has changed.

Please give me a feedback.


===== UPDATE =====

There is a less intrusive solution, which consists in defining an exit method in Chef::Knife.

When you see exit 100, i.e. a message without receiver, the implicit receiver is self, it is equivalent to self.exit 100. In our case, self is the object created by instance = subcommand_class.new(args), and which is the receiver in instance.run_with_pretty_exceptions.

When a message is sent to an object, the message search mechanism starts looking in the class of this object. If there is no method with this name in the class, the search mechanism looks in included modules, then the superclass, etc until it reaches Object, the default superclass of Chef::Knife. Here it finds Object#exit and executes it.

After defining an exit method in Chef::Knife, the message search mechanism, when it encounters exit 100 with an instance of Chef::Knife as implicit receiver, will first find this local method and execute it. By previously aliasing the original Object#exit, it is still possible to call the original Ruby method which initiates the termination of the Ruby script. This way the local exit method can decide either to call the original Object#exit or take other action.

Following is a complete example which demonstrates how it works.

# ***** 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

Execution. First test with 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

Second test with a hard interrupt.

In one terminal window :

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

In another terminal 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

Back in the first terminal :

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>

Considering the code you originally posted, all you have to do is inserting at the beginning, but after the necessary 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

So there is no need to touch the original gem.

Upvotes: 1

Big X
Big X

Reputation: 98

The following code shows how I am able to interrupt after all:

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

The drawback still is, that I am not exactly able to interrupt the Knife.run, but only able to trap the interrupt and check after that command whether an interrupt was triggered. I found no way to trap the interrupt and "reraise" it at the same time, so that I am at least able to force an exit out of Knife.run which I can then exit manually.

Upvotes: 0

Related Questions