Interrupt raw_input in a twisted program

993 views Asked by At

I will refer to this explanation and this workaround:

So what I am doing is:

def interrupted(signum, stackframe):
    log.warning('interrupted > Got signal: %s', signum)
    menu.quitMenu = True # to stop my code

signal.signal(signal.SIGINT, interrupted) # Handle KeyboardInterrupt

The problem is that, while the menu is notified that it must stop, and will do that soon, it can not do it now since it is stucked in a raw_input:

def askUser(self):
    current_date   = datetime.now().isoformat(' ')
    choice = raw_input('%s > ' % current_date)
    return choice

So, since twisted is removing the default interrupt handler, raw_input is not stopped. I still need to press enter after ^C for it to stop.

How can I force the raw_input to be stopped, without installing the default interrupt handler, which is a source of problems in a twisted context (since twisted itself is not expecting to be interrupted)

I guess the problem is not related to raw_input only: any function taking an unbounded time (or longer than a pre-set limit), should be interrupted somehow.

Is there an accepted twisted pattern for this?

EDIT

This is the full test code:

from datetime import datetime

class Menu:

    def __init__(self):
        self.quitMenu = False

    def showMenu(self):
        print '''

A) Do A
B) Do B

'''

    def askUser(self):
        current_date   = datetime.now().isoformat(' ')
        choice = raw_input('%s > Please select option > ' % current_date)
        print
        return choice

    def stopMe(self):
        self.quitMenu = True

    def alive(self):
        return self.quitMenu == False

    def doMenuOnce(self):
        self.showMenu()
        choice = self.askUser()
        if not self.alive() : # Maybe somebody has tried to stop the menu while in askUser
            return
        if   choice == 'A' : print 'A selected'
        elif choice == 'B' : print 'B selected'
        else               : print 'ERR: choice %s not supported' % (choice)

    def forever(self):
        while self.alive():
            self.doMenuOnce()

from twisted.internet import reactor, threads
import signal

class MenuTwisted:

    def __init__(self, menu):
        self.menu = menu
        signal.signal(signal.SIGINT, self.interrupted) # Handle KeyboardInterrupt

    def interrupted(self, signum, stackframe):
        print 'Interrupted!'
        self.menu.stopMe()

    def doMenuOnce(self):
        threads.deferToThread(self.menu.doMenuOnce).addCallback(self.forever)

    def forever(self, res=None):
        if self.menu.alive() :
            reactor.callLater(0, self.doMenuOnce)
        else : 
            reactor.callFromThread(reactor.stop)

    def run(self):
        self.forever()
        reactor.run()

Which I can run in two different ways.

Normal way:

menu = Menu()
menu.forever()

Pressing ^C stops the program immediately:

A) Do A
B) Do B


2013-12-03 11:00:26.288846 > Please select option > ^CTraceback (most recent call last):
  File "twisted_keyboard_interrupt.py", line 72, in <module>
    menu.forever()
  File "twisted_keyboard_interrupt.py", line 43, in forever
    self.doMenuOnce()
  File "twisted_keyboard_interrupt.py", line 34, in doMenuOnce
    choice = self.askUser()
  File "twisted_keyboard_interrupt.py", line 22, in askUser
    choice = raw_input('%s > Please select option > ' % current_date)
KeyboardInterrupt

As expected.

Twisted way:

menu = Menu()
menutw = MenuTwisted(menu)
menutw.run()

Pressing ^C will produce:

A) Do A
B) Do B


2013-12-03 11:04:18.678219 > Please select option > ^CInterrupted!

But askUser is actually not interrupted: I still need to press enter for raw_input to finish.

1

There are 1 answers

0
Glyph On BEST ANSWER

The right way to deal with this is to handle console input asynchronously, rather than trying to make a blocking input function interruptable. In other words, raw_input is fundamentally the wrong solution to the problem you're attacking.

However, if you really just want to understand what's going on here, the trick is that after calling reactor.callFromThread(reactor.stop), you have to somehow prompt raw_input to exit; it's not going to normally. However, since you're running it in a thread, it's not actually interruptable at all, because only the main thread is interruptable in Python. So I think what you want may actually be impossible. I believed that perhaps closing sys.stdin might pull the rug out from under raw_input even if it were in a thread, but it seems that the underlying libraries are doing something more clever than simply reading from the FD, so closing it does no good.