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!!

9
  • To clarify, you want to send arbitrary keystrokes to a running Python script on a Unix-like OS? Commented Dec 21, 2015 at 13:21
  • exactly! If there's something bad written or grammatically incorrect, please edit! Commented Dec 21, 2015 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? Commented Dec 21, 2015 at 13:23
  • 1
    You might take a look at: 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. Commented Dec 21, 2015 at 13:41
  • 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. Commented Dec 21, 2015 at 13:55

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Awesome!! Thank you very much for the effort!!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.