1

I am looking for the way of sending a keystroke to a Python script. In this case, I am trying that the script detects if a press whatever key, and not only the interrupt signals (ctrl + c , ctrl + d, ...).

I have checked the signal python module. But it seems like it's only prepared to handle interrupt signals, and not if I press "K" or "Space" for example. I have seen this in the official docs of the module:

import signal
import os
import time

def receive_signal(signum, stack):
    print 'Received:', signum

signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

print 'My PID is:', os.getpid()

while True:
    print 'Waiting...'
    time.sleep(3)

And they say:

To send signals to the running program, I use the command line program kill. To produce the output below, I ran signal_signal.py in one window, then kill -USR1 $pid, kill -USR2 $pid, and kill -INT $pid in another.

I am quite sure that this module is not the solution. Do you know some module or something that could help me for sending keystroke to my python script asynchronously ?

Thanks a lot!!

Luis González
  • 3,199
  • 26
  • 43
  • To clarify, you want to send arbitrary keystrokes to a running Python script on a Unix-like OS? – Aya Dec 21 '15 at 13:21
  • exactly! If there's something bad written or grammatically incorrect, please edit! – Luis González Dec 21 '15 at 13:22
  • Well, you just seem to use the word "signal" a lot, and a signal is not the same thing as a keystroke. Can the program you want to receive the keystrokes be started by the script you want to use to send them? – Aya Dec 21 '15 at 13:23
  • I don't want to use an script to send keystroke. I want launch my script by typing `python myscript.py`. After that, I would like be able to press a key and the script prints "You have pressed the key K" (just an example) – Luis González Dec 21 '15 at 13:31
  • 1
    You might take a look at: http://stackoverflow.com/a/6599441/3991125 Please clarify the difference between signals and keyboard inputs / keystrokes to yourself since _signals_ are something the OS uses internally in order to _talk_ to processes. – albert Dec 21 '15 at 13:41
  • I guess that I am trying convert a keystroke to a signal that talk to a python process, in this case my script. In the link you gave me it seems like the scripts wait for a keystroke and I am trying that the scripts handle the keystroke asynchronously. – Luis González Dec 21 '15 at 13:47
  • You might also want to take a look at the [`curses`](https://docs.python.org/2/library/curses.html) module. – Aya Dec 21 '15 at 13:47
  • Is there some particular reason you need to use signals? Can you be clearer about the specific use-case? – Aya Dec 21 '15 at 13:49
  • 1
    Yes. I have a `for` looping over all the months of the year. For each month, the script loops over all the days in the month. For each day in the month, the script loops over a certain number of machines and starts making operations about the consumption and prints the results . I want the user has the possibility of skip a day, a month or a machine by pressing a key in whatever moment. And not very sure that this would be possible. – Luis González Dec 21 '15 at 13:55

1 Answers1

2

I want the user has the possibility of skip a day, a month or a machine by pressing a key in whatever moment.

Ah. Now it makes sense.

And not very sure that this would be possible.

Anything's possible. It can just be quite complex for a truly asynchronous solution.

The only way I could think to do it, while avoiding a polling approach, was to fork(2) the process, have the parent process listen for keypresses, and send signals to the child process, which actually does the work.

Something like this...

#!/usr/bin/env python

import sys, os, time, termios, tty, signal


# Define some custom exceptions we can raise in signal handlers
class SkipYear(Exception):
    pass

class SkipMonth(Exception):
    pass


# Process one month
def process_month(year, month):

    # Fake up whatever the processing actually is
    print 'Processing %04d-%02d' % (year, month)
    time.sleep(1)


# Process one year
def process_year(year):

    # Iterate months 1-12
    for month in range(1, 13):

        try:
            process_month(year, month)
        except SkipMonth:
            print 'Skipping month %d' % month


# Do all processing
def process_all(args):

    # Help
    print 'Started processing - args = %r' % args

    try:

        # Iterate years 2010-2015
        for year in range(2010, 2016):

            try:
                process_year(year)
            except SkipYear:
                print 'Skipping year %d' % year

    # Handle SIGINT from parent process
    except KeyboardInterrupt:
        print 'Child caught SIGINT'

    # Return success
    print 'Child terminated normally'
    return 0


# Main entry point
def main(args):

    # Help
    print 'Press Y to skip current year, M to skip current month, or CTRL-C to abort'

    # Get file descriptor for stdin. This is almost always zero.
    stdin_fd = sys.stdin.fileno()

    # Fork here
    pid = os.fork()

    # If we're the child
    if not pid:

        # Detach child from controlling TTY, so it can't be the foreground
        # process, and therefore can't get any signals from the TTY.
        os.setsid()

        # Define signal handler for SIGUSR1 and SIGUSR2
        def on_signal(signum, frame):
            if signum == signal.SIGUSR1:
                raise SkipYear
            elif signum == signal.SIGUSR2:
                raise SkipMonth

        # We want to catch SIGUSR1 and SIGUSR2
        signal.signal(signal.SIGUSR1, on_signal)
        signal.signal(signal.SIGUSR2, on_signal)

        # Now do the thing
        return process_all(args[1:])

    # If we get this far, we're the parent

    # Define a signal handler for when the child terminates
    def on_sigchld(signum, frame):
        assert signum == signal.SIGCHLD
        print 'Child terminated - terminating parent'
        sys.exit(0)

    # We want to catch SIGCHLD
    signal.signal(signal.SIGCHLD, on_sigchld)

    # Remember the original terminal attributes
    stdin_attrs = termios.tcgetattr(stdin_fd)

    # Change to cbreak mode, so we can detect single keypresses
    tty.setcbreak(stdin_fd)

    try:

        # Loop until we get a signal. Typically one of...
        #
        # a) SIGCHLD, when the child process terminates
        # b) SIGINT, when the user presses CTRL-C
        while 1:

            # Wait for a keypress
            char = os.read(stdin_fd, 1)

            # If it was 'Y', send SIGUSR1 to the child
            if char.lower() == 'y':
                os.kill(pid, signal.SIGUSR1)

            # If it was 'M', send SIGUSR2 to the child
            if char.lower() == 'm':
                os.kill(pid, signal.SIGUSR2)

    # Parent caught SIGINT - send SIGINT to child process
    except KeyboardInterrupt:
        print 'Forwarding SIGINT to child process'
        os.kill(pid, signal.SIGINT)

    # Catch system exit
    except SystemExit:
        print 'Caught SystemExit'

    # Ensure we reset terminal attributes to original settings
    finally:
        termios.tcsetattr(stdin_fd, termios.TCSADRAIN, stdin_attrs)

    # Return success
    print 'Parent terminated normally'
    return 0


# Stub
if __name__ == '__main__':
    sys.exit(main(sys.argv))

...should do the trick, although you'll be limited by the number of distinct signals you can send.

Aya
  • 39,884
  • 6
  • 55
  • 55