0

EDIT: Narrowed down problem from original version, originally assumed all SIGINT overrides were being ignored, but it's actually just the subprocess one, edited to reflect this.

I'd like to have python shutdown safely when receiving the SIGINT (Ctrl+C) from systemd. However, the command sudo systemctl kill --signal=SIGINT myapp ignores my subprocess.Popen(args, stdout=PIPE, stderr=PIPE, preexec_fn = os.setsid) line, which prevents the SIGINT from going to a called process (works when NOT using systemd), and crashes my program anyways.

Here's my setup (similar to this: How can I make a python daemon handle systemd signals?):

shutdown = False

def shutdown_handler(signal, frame):

    global shutdown

    is_thread = frame.f_code.co_name == "my_thread_func"
    if shutdown:
        logging.info("Force shutdown for process {0}".format(os.getpid()))
        raise KeyboardInterrupt
    else:
        shutdown = True
        if not is_thread:
            logging.info("Shutdown signal received. Waiting for sweeps to finish.")
            logging.info("Press Ctrl-C again to force shutdown.")
        return

signal.signal(signal.SIGINT, shutdown_handler)

Elsewhere: subprocess.Popen(args, stdout=PIPE, stderr=PIPE, preexec_fn = os.setsid)

When running NOT using systemd (as just python daemon.py), the Popen subprocess continues running as desired. But when using sudo systemctl kill --signal=SIGINT myapp, it sends the signal to the parent, child, and Popen (command line) processes.

systemd[1]: fi_iot.service: Sent signal SIGINT to main process 512562 (python3) on client request.
systemd[1]: fi_iot.service: Sending signal SIGINT to process 512978 (python3) on client request.
systemd[1]: fi_iot.service: Sending signal SIGINT to process 513023 (my-cli-tool) on client request.

Any one know why this is happening?

I'm also open to suggestions on alternative ways of implementing this (eg adding an ExecStop= arg to my system config, or using a custom signal instead of SIGINT), though I'd rather override as little default behavior as possible, I want sudo systemctl stop myapp to do what it's supposed to do without my custom code potentially messing things up or confusing others.

EDIT: It seems this issue is specific to how the Popen function is called, I might try setting it to SIGIGN and see it that works, an earlier version of this post indicated this was a broader issue than it appears to be.

  • It sounds like `systemd` is sending the signal twice, like when you type Ctl-c twice. Maybe it has a timeout to retry if the process doesn't exit, and you're taking too long. – Barmar Jan 24 '23 at 22:45
  • I may be quite mistaken, but doesn't systemd.kill by default [kill each process within a cgroup](https://stackoverflow.com/q/40898077/132382)? Short of `--kill-whom` or `KillMode=` changes, this behavior is expected. – pilcrow Jan 25 '23 at 03:37

2 Answers2

0

In python3 ctl-c is raised as the error KeyboardInterrupt

to catch it use try, and except KeyboardInterrupt:

The best bet to catching it is to make a main method, and put the try except around where you call it.

Update:

Popen has a method called send_signal, so you can forward the systemd signal to it

relevant python docs https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal

0

Solution: Using a different method of preventing subprocess.Popen from overriding signals worked:

def preexec_function():
    # used by Popen to tell driver to ignore SIGINT
    signal.signal(signal.SIGINT, signal.SIG_IGN)

proc = Popen(args, stdout=PIPE, stderr=PIPE, preexec_fn = preexec_function)

Now the subprocess ignores the signal, unlike copilot's preexec_fn=os.setsid, which doesn't do what I want from systemd, which is what I get for using GPT-3 generated code I don't understand.

I may look into using Showierdata9978's suggestion of using send_signal, which could allow me to send the interrupt when the second Ctrl+C is pressed, allowing it to shutdown safely despite the ignore.