1

So a bit of an odd project where I'm attempting to use a subprocess to keep track of the number of keys I press to measure my productivity.

Currently I start the subprocess using an Amazon Dash button, then kill the process on the second press.

def start_keylogger():
    global process
    process = subprocess.Popen(["sudo", "python", "test.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def stop_keylogger():
    os.killpg(process.pid, signal.SIGUSR1)
    process.wait()
    key_press_count = process.stdout.read()
    return key_press_count

From there my keylogger hasn't been fleshed out quite yet, but I think I would like to use sys.exit() or exit() to return the number of key presses.

def exit_and_return_counter():
    sys.stdout.write(current_keypress_counter)
    exit()

if __name__ == '__main__':
    signal.signal(signal.SIGUSR1, exit_and_return_counter)
    try:
        while 1:
           main_loop()
    except KeyboardInterrupt:
        sys.exit()

Initially I tried to use process.returncode, but it only returned 1, I'm assuming the successful exit code. And I can't use stdout, stderr = process.communicate() unless I want to keep the keylogger a short while after the second Amazon button press.

  • You can only get the return code if the process exits normally with `exit(code)`. WHen you kill it, the process exits due to the signal, and doesn't get to send an exit code. – Barmar Oct 04 '15 at 04:48
  • 1
    BTW, on Unix the largest possible exit code is `255`. So your technique is not useful if you might type more than that many keys. – Barmar Oct 04 '15 at 04:49
  • @Barmar: even if the process has been kill; you can get the return code (`rc = process.wait()` -- it is probably `-9` (corresponds to `SIGKILL`)). Though OP's code has other issues. – jfs Oct 04 '15 at 08:03
  • there are multiple issues in your code e.g., (1) It is pointless to import `call` if you need `subprocess.Popen` (2) [don't use `shell=True` and the list argument together on Unix](http://stackoverflow.com/q/2400878/4279) (3) `global` inside `stop_keylogger()` is unnecessary (there is no assignment) (4) `process.kill()` won't kill the whole process tree, see [How to terminate a python subprocess launched with shell=True](http://stackoverflow.com/q/4789837/4279) (5) KeyboardInterrupt won't catch SIGKILL (6) as @Barmar said: exit status range might be too small – jfs Oct 04 '15 at 08:11
  • @J.F.Sebastian But you can't return a **specific** code using `sys.exit()` if the process is killed. – Barmar Oct 04 '15 at 16:17
  • Excellent, thanks for the help @Barmar and @J.F. Sebastian. So I guess the better question would be how should I stop the process so I could use `exit(code)` to retrieve the value. I'm considering, while I think it's not the greatest idea, only running the keylogger for fixed intervals, say five minutes, and then adding the results together. – BraedenYoung Oct 04 '15 at 21:05
  • you should fix one issue at a time. You could start by following the links from my previous comment. – jfs Oct 05 '15 at 01:04
  • @BraedenYoung You could send a signal that doesn't kill the process, like `SIGUSR1`. And the signal handler in the keylogger can then do an ordinary exit. Or use a real inter-process communication mechanism like a pipe to tell the keylogger to exit. – Barmar Oct 05 '15 at 05:47
  • Sorry @J.F.Sebastian I thought I had. Anyhow, following along with the stream of comments it looks like the best way to go about this would be to use `SIGUSR1` and handle the exit in the keylogger script. I assumed that it would still be something along the lines of `process.returncode` but that only results in a None. I imagine I may not be handling the the `SIGUSR1` right in my keylogger script. – BraedenYoung Oct 07 '15 at 00:04
  • @Barmar the process isn't killed, it terminates normally. `SIGUSR1` is catched and thus doesn't kill the process. The `sys.exit(code)` terminates it. – cg909 Oct 07 '15 at 01:59
  • @cg909 `SIGUSR1` wasn't in the question when I made my comment, he edited the question today to add it. – Barmar Oct 07 '15 at 04:06
  • I've missed that you've updated the question two days ago. (1) don't use `PIPE` unless you consume the corresponding pipe otherwise you may hang the child process. [Use `DEVNULL` if you need to disgard the output](http://stackoverflow.com/q/11269575/4279) (2) if there is no `shell=True` then you don't need `os.killpg()` (and it is wrong without `preexec_fn=os.setpgrp`) (3) why do you need a subprocess here instead of [importing the module and using its functions](http://stackoverflow.com/a/30165768/4279) – jfs Oct 07 '15 at 10:19
  • @Barmar ok, you're right I didn't see that. It seems at first that the OP mixed what will happen on `.kill()` and when `KeyboardInterrupt` is catched. His first version would have worked with `.send_signal(SIGINT)` – cg909 Oct 07 '15 at 13:35
  • @J.F. Sebastian, (1) ah, good to know, I'm using it now in my answer below. (2) That's probably why I was struggling, it seemed like it would hang. (3) Since I have a main loop checking for new arps, and a another main loop recording key presses. I imagine I could have reworked it so I conditionally record key presses, but this seemed a lot easier. This also allows me to play with other keyloggers easily, or even swap out other python modules if I was to make an generic api to return a value. – BraedenYoung Oct 07 '15 at 14:04

3 Answers3

1

Simply run process.wait() after os.killpg(process.pid, signal.SIGUSR1). This waits for the process to end and returns the status code. As you killed it just moments before, it won't block for long. Catching the USR1 signal and using sys.exit to change the code returned from the child should work.

You should also be able to read stdout with process.stdout.read() even after the child was killed, as the pipe created for the inter-process communication will live at least as long as the Popen object process exists.

cg909
  • 2,247
  • 19
  • 23
  • So waiting on the process definitely seemed to be an issue, but I'm not getting anything back from `exit(current_keypress_count)`. I'm assuming I must not be handling `signal.SIGUSR1` right. Just to be clear, `process.stdout.read()` wasn't returning anything and `process.returncode` was 1 – BraedenYoung Oct 07 '15 at 01:09
  • Try removing `stderr=subprocess.PIPE` from your Popen call. Then you can see the Python exceptions your subprocess is throwing. :-) I found the following issues: a signal callback must accept two parameters: the signal number and a stack frame, so: `exit_and_return_counter(signo, frame):` and `sys.stdout.write()` needs a string, so use sys.stdout.write(str(current_keypress_counter)). Also on my computer `os.killpg()` didn't work. `process.send_signal(signal.SIGUSR1)` worked for me. – cg909 Oct 07 '15 at 01:50
  • Awesome, yeah I was able to handle the `SIGUSR1` when sent with `process.send_signal(signal.SIGUSR1)`. I put my changes in an answer below. Thanks for the help @cg909 – BraedenYoung Oct 07 '15 at 13:55
1

While not an elegant answer, using process.send_signal(signal.SIGUSR1) worked (as mentioned by @cg909). In the stop method we have:

def stop_keylogger():
    process.send_signal(signal.SIGUSR1)
    process.wait()
    return process.stderr.read()

Then in the key logger to handle the signal we have:

def exit_and_return_counter(*args):
    sys.stdout.write('%s'%counter)
    exit()

if __name__ == '__main__':
    signal.signal(signal.SIGUSR1, exit_and_return_counter)
    try:
        while 1:
            main_loop()
    except KeyboardInterrupt:
        sys.exit()
0

You could use SIGINT signal (Ctrl+C in the terminal) to signal the process to print statistics and exit. You could handle SIGINT using KeyboardInterrupt exception handler:

#!/usr/bin/env python
#file: keylogger.py
import random
import time

counter = 0

def mainloop()
    global counter
    while True:
        counter += 1
        time.sleep(random.random())

if __name__ == '__main__':
    try:
        mainloop()
    except KeyboardInterrupt: # on SIGINT
        print(counter)

And here's the corresonding controller:

#!/usr/bin/env python3
import os
import signal
from subprocess import Popen, PIPE, DEVNULL

process = None

def start_keylogger():
    global process
    stop_keylogger(expect_process_running=False)
    process = Popen(["python", "-m", "keylogger"], stdout=PIPE, stderr=DEVNULL,
                    preexec_fn=os.setpgrp)

def stop_keylogger(expect_process_running=True):
    global process
    if process is not None and process.poll() is None:
        os.killpg(process.pid, signal.SIGINT)
        counter = int(process.communicate()[0])
        process = None
        return counter
    if expect_process_running:
        raise RuntimeError('keylogger is not running')

You could emulate sending SIGINT when you run keylogger.py in the terminal (for testing/debugging) by pressing Ctrl+C.

preexec_fn=os.setpgrp creates a separate process group. os.killpg() sends it the signal. If your keylogger.py does not spawn its own child processes when preexec_fn=os.setpgrp is unnecessary and it is enough to call process.send_signal(signal.SIGINT) instead of os.killpg().

Consider importing the keylogger module and using its functions instead of using subprocess.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670