3

Is there a way to send an interrupt to a python module when the user inputs something in the console? For example, if I'm running an infinite while loop, i can surround it with a try/except for KeyboardInterrupt and then do what I need to do in the except block.

Is there a way to duplicate this functionality with any arbitrary input? Either a control sequence or a standard character?

Edit: Sorry, this is on linux

Falmarri
  • 47,727
  • 41
  • 151
  • 191

6 Answers6

2

You need a separate process (or possibly a thread) to read the terminal and send it to the process of interest via some form of inter-process communication (IPC) (inter-thread communication may be harder -- basically the only thing you do is send a KeyboardInterrupt from a secondary thread to the main thread). Since you say "I was hoping there would be a simple way to inject user input into the loop other than just ^C", I'm sad to disappoint you, but that's the truth: there are ways to do what you request, but simple they aren't.

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
2

Dependent on the operating system and the libraries available, there are different ways of achieving that. This answer provides a few of them.

Here is the Linux/OS X part copied from there, with in infinite loop terminated using the escape character. For the Windows solution you can check the answer itself.

import sys
import select
import tty
import termios

from curses import ascii

def isData():
    return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])

old_settings = termios.tcgetattr(sys.stdin)
try:
    tty.setcbreak(sys.stdin.fileno())

    i = 0
    while 1:
        print i
        i += 1

        if isData():
            c = sys.stdin.read(1)
            if c == chr(ascii.ESC):
                break

finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

Edit: Changed the character detection to use characters as defined by curses.ascii, thanks to Daenyth's unhappiness with magic values which I share.

Community
  • 1
  • 1
Muhammad Alkarouri
  • 23,884
  • 19
  • 66
  • 101
  • I feel like that `\x1b` should be a `KeyCodes.ESC` or a `key_codes['ESC']` defined elsewhere... Boo to magic values! – Daenyth Sep 17 '10 at 18:38
  • @Daenyth: I know the feeling. Things should be better now. – Muhammad Alkarouri Sep 17 '10 at 18:55
  • @Muhammed Alkarouri: And thanks to you for informing me of a module I didn't know about :) – Daenyth Sep 17 '10 at 19:17
  • 2
    This didn't work for me (in Mac OS X but probably doesn't matter) until I switched back to the magic value '\x1b'. ascii.ESC = 27, and chr(27) = '\x1b'... so maybe you just need chr(ascii.ESC). I confirmed that this change works for me and edited your code accordingly, feel free to revert if I missed something! – murftown Jul 12 '13 at 17:22
  • 1
    Or rather, anyone with the proper rep/access, feel free to review my fix and approve it! I'm pretty sure this code has been broken since 2010 for the reason described above. – murftown Jul 12 '13 at 17:36
  • 1
    So one person approved my edit, but 3 people rejected it, and no way to message them -- again, pretty sure this code is broken and has been for 3 years. Keyboard input in python is a pretty important thing IMHO. Could Andrew, soon or vals please respond to tell me what is wrong with my fix, or if they have done actual testing? I will try to resubmit the edit. Thanks! – murftown Jul 12 '13 at 20:28
  • 1
    I just tested on linux and can confirm what murftown is saying. The code doesn't work as is, only if `ascii.ESC` is replaced with `chr(ascii.ESC)`. – evanrmurphy Jul 12 '13 at 20:33
  • Cool, Guido corrected it for me, he has more rep so he can get his edit through. Thanks Guido! – murftown Jul 12 '13 at 21:55
  • 1
    Thanks @murftown. Haven't tried it but from reviewing my edits, it is clear that this is a bug introduced by an edit of mine since 2010 as you said. Good catch! – Muhammad Alkarouri Jul 12 '13 at 23:29
  • 1
    Happens to the best of us Muhammad, thanks for the great answer! – murftown Jul 12 '13 at 23:53
1

EDIT: Guido made the edit for me on the accepted answer, so this answer is no longer necessary.

The accepted answer is no longer working because of a change Muhammed made. I have tried to submit a correction, but it keeps getting rejected, so I will post it as a separate answer. My code is almost identical to his, just 1 tiny change:

import sys
import select
import tty
import termios

from curses import ascii

def isData():
    return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])

old_settings = termios.tcgetattr(sys.stdin)
try:
    tty.setcbreak(sys.stdin.fileno())

    i = 0
    while 1:
        print i
        i += 1

        if isData():
            c = sys.stdin.read(1)
            if c == chr(ascii.ESC):
                break

finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

The only difference is instead of "c == ascii.ESC" I changed it to "c == chr(ascii.ESC). I and 1 other developer both have tested and confirmed that this change is necessary and that otherwise the program will not work correctly.

The program is supposed to show bigger and bigger numbers until you hit ESC, and then exit. But without the chr() around the ascii.ESC, it will not detect your ESC keypress.

murftown
  • 1,287
  • 1
  • 10
  • 13
1

KeyboardInterrupt is special in that it can be trapped (i.e. SIGINT under operating systems with respective POSIX support, SetConsoleCtrlHandler on Windows) and handled accordingly. If you want to process user input while at the same time doing work in a otherwise blocking loop, have a look at the threading or subprocess modules, taking a look at how to exchange data between two different threads / processes. Also be sure to get an understanding of the issues that go hand in hand with parallel computing (i.e. race conditions, thread safety / synchronization etc.)

Jim Brissom
  • 31,821
  • 4
  • 39
  • 33
  • Yeah I'm familiar with threading and all that, I'm just working on a test function right now and I was hoping there would be a simple way to inject user input into the loop other than just ^C – Falmarri Sep 16 '10 at 23:54
  • Let it be known that interacting with users is **never** simple! – jathanism Sep 17 '10 at 00:50
1

I'm not sure if it's the most optimal solution but you can create a thread that does a while True: sys.stdin.read(1)

That way you can always read all input, but it will be slow and you'll have to combine the strings yourself.

Example:

import os
import sys
import time
import threading

class StdinReader(threading.Thread):
    def run(self):
        while True:
            print repr(sys.stdin.read(1))

os.system('stty raw')

stdin_reader = StdinReader()
stdin_reader.start()

while True:
    time.sleep(1)

os.system('stty sane')

The stty raw thing depends on where you're running it though. It won't work everywhere.

Wolph
  • 78,177
  • 11
  • 137
  • 148
0

This is how I implemented the functionality I wanted, but it's not EXACTLY what my question is about. So I'll post this in case anyone is looking for the same concept, but accept one of the more direct answers.


while True:
        try:
            time.sleep( 1 )
            #do stuff
        except KeyboardInterrupt:
            inp = raw_input( '>' )
            if inp == 'i':
                obj.integrate()
            elif inp == 'r':
                obj.reset()
Falmarri
  • 47,727
  • 41
  • 151
  • 191