Capturer Ctrl-c dans ruby

107

J'ai reçu un programme ruby ​​hérité de longue date, qui a de nombreuses occurrences de

begin
  #dosomething
rescue Exception => e
  #halt the exception's progress
end

partout.

Sans rechercher toutes les exceptions possibles, celles-ci pourraient être gérées (du moins pas immédiatement), j'aimerais toujours pouvoir l'arrêter parfois CtrlC.

Et j'aimerais le faire d'une manière qui ne fait qu'ajouter au code (donc je n'affecte pas le comportement existant, ou je rate une exception autrement capturée au milieu d'une exécution.)

[ CtrlCest SIGINT, ou SystemExit, qui semble être équivalent à SignalException.new("INT")dans le système de gestion des exceptions de Ruby. class SignalException < Exception, c'est pourquoi ce problème se pose.]

Le code que j'aimerais avoir écrit serait:

begin
  #dosomething
rescue SignalException => e
  raise e
rescue Exception => e
  #halt the exception's progress
end

EDIT: Ce code fonctionne, tant que vous obtenez la classe de l'exception que vous souhaitez intercepter correctement. C'est soit SystemExit, Interrupt ou IRB :: Abort comme ci-dessous.

Tim Snowhite
la source

Réponses:

132

Le problème est que lorsqu'un programme Ruby se termine, il le fait en lançant SystemExit . Quand un contrôle-C entre, il déclenche Interruption . Étant donné que SystemExit et Interrupt dérivent d' Exception , votre gestion des exceptions arrête la sortie ou l'interruption dans ses pistes. Voici la solution:

Partout où vous pouvez, changez

rescue Exception => e
  # ...
end

à

rescue StandardError => e
  # ...
end

pour ceux que vous ne pouvez pas changer en StandardError, relancez l'exception:

rescue Exception => e
  # ...
  raise
end

ou, à tout le moins, relancez SystemExit et Interruption

rescue SystemExit, Interrupt
  raise
rescue Exception => e
  #...
end

Toutes les exceptions personnalisées que vous avez faites doivent dériver de StandardError , pas d' Exception .

Wayne Conrad
la source
1
Wayne, auriez-vous l'amabilité d'ajouter un exemple IRB :: Abort à votre liste également?
Tim Snowhite
1
@Tim, va chercher irb.rb (sur mon système, il est dans /usr/lib/ruby/1.8/irb.rb) et trouve la boucle principale (recherche @ context.evaluate). Regardez les clauses de sauvetage et je pense que vous comprendrez pourquoi la CISR se comporte comme elle le fait.
Wayne Conrad
Merci. Regarder la définition de #signal_handle dans irb.rb m'a également aidé à comprendre. Ils ont également une astuce intéressante dans la liaison de variable d'exception de la boucle principale. (Utiliser les clauses de sauvetage comme moyen de sélectionner une exception spécifique, puis utiliser cette exception en dehors des corps de sauvetage.)
Tim Snowhite
ces œuvres parfaites:rescue SystemExit, Interrupt raise rescue Exception => e
James Tan
73

Si vous pouvez encapsuler tout votre programme, vous pouvez faire quelque chose comme ce qui suit:

 trap("SIGINT") { throw :ctrl_c }

 catch :ctrl_c do
 begin
    sleep(10)
 rescue Exception
    puts "Not printed"
 end
 end

Cela CtrlCutilise essentiellement catch / throw au lieu de la gestion des exceptions, donc à moins que le code existant ne contienne déjà un catch: ctrl_c, cela devrait aller.

Vous pouvez également faire un trap("SIGINT") { exit! }. exit!se termine immédiatement, il ne lève pas d'exception, donc le code ne peut pas l'attraper accidentellement.

Logan Capaldo
la source
2
Notez que Ctrl-C dans IRB envoie IRB :: Abort, pas SIGINT. Sinon, la réponse de @ Logan est une solution.
Tim Snowhite
1
@TimSnowhite pour l'interpréteur ruby SIGINTfonctionne très bien pour moi.
défhlt le
1
throw et catch doivent être sur le même thread, donc cela ne fonctionnera pas si vous voulez intercepter l'exception Interrupt sur un autre thread.
Matt Connolly
39

Si vous ne pouvez pas envelopper toute votre application dans un begin ... rescuebloc (par exemple, Thor), vous pouvez simplement piéger SIGINT:

trap "SIGINT" do
  puts "Exiting"
  exit 130
end

130 est un code de sortie standard.

Erik Nomitch
la source
1
Pour info, 130 est le code de sortie correct pour les scripts interrompus Ctrl-C: google.com/search?q=130+exit+code&en= ( 130 | Script terminated by Control-C | Ctl-C | Control-C is fatal error signal 2, (130 = 128 + 2, see above))
Dorian
Parfait! J'ai un serveur Sinatra bancal avec un thread d'arrière-plan constamment en cours d'exécution, et cela ressemble à ce dont j'ai besoin pour tuer le thread également sur un cntrl-c, sans autrement changer de comportement.
Narfanator
4

J'utilise ensureà bon escient! C'est pour les choses que vous voulez faire se produire lorsque vos affaires se terminent, peu importe pourquoi elles se terminent.

nez
la source
0

Manipuler proprement Ctrl-C dans Ruby à la manière ZeroMQ:

#!/usr/bin/env ruby

# Shows how to handle Ctrl-C
require 'ffi-rzmq'

context = ZMQ::Context.new(1)
socket = context.socket(ZMQ::REP)
socket.bind("tcp://*:5558")

trap("INT") { puts "Shutting down."; socket.close; context.terminate; exit}

puts "Starting up"

while true do
  message = socket.recv_string
  puts "Message: #{message.inspect}"
  socket.send_string("Message received")
end

La source

Noraj
la source
Bel exemple, mais je pense que cela ajoute plus de complexité que ce qui est réellement nécessaire dans le contexte d'OP.
Ron Klein