45

I am currently working on a wrapper for a dedicated server running in the shell. The wrapper spawns the server process via subprocess and observes and reacts to its output.

The dedicated server must be explicitly given a command to shut down gracefully. Thus, CTRL-C must not reach the server process.

If I capture the KeyboardInterrupt exception or overwrite the SIGINT-handler in python, the server process still receives the CTRL-C and stops immediately.

So my question is: How to prevent subprocesses from receiving CTRL-C / Control-C / SIGINT?

robert
  • 3,484
  • 3
  • 29
  • 38

5 Answers5

43

Somebody in the #python IRC-Channel (Freenode) helped me by pointing out the preexec_fn parameter of subprocess.Popen(...):

If preexec_fn is set to a callable object, this object will be called in the child process just before the child is executed. (Unix only)

Thus, the following code solves the problem (UNIX only):

import subprocess
import signal

def preexec_function():
    # Ignore the SIGINT signal by setting the handler to the standard
    # signal handler SIG_IGN.
    signal.signal(signal.SIGINT, signal.SIG_IGN)

my_process = subprocess.Popen(
    ["my_executable"],
    preexec_fn = preexec_function
)

Note: The signal is actually not prevented from reaching the subprocess. Instead, the preexec_fn above overwrites the signal's default handler so that the signal is ignored. Thus, this solution may not work if the subprocess overwrites the SIGINT handler again.

Another note: This solution works for all sorts of subprocesses, i.e. it is not restricted to subprocesses written in Python, too. For example the dedicated server I am writing my wrapper for is in fact written in Java.

codeling
  • 11,056
  • 4
  • 42
  • 71
robert
  • 3,484
  • 3
  • 29
  • 38
  • 1
    `Popen(preexec_fn=...)` [will eventually cause a deadlock](https://github.com/python/cpython/blob/036bb7365607ab7e5cf901f1ac4256f9ae1be82c/Modules/_posixsubprocess.c#L932-L938) if used in a python application with multiple threads. I have hit this a few times, and it's documented here: https://docs.python.org/3/library/subprocess.html#subprocess.Popen – Adam Casey Jul 20 '23 at 10:39
26

Combining some of the other answers that will do the trick - no signal sent to main app will be forwarded to the subprocess.

On Python 3.11 and newer you can use the process_group argument for Popen:

from subprocess import Popen

Popen('do_not_want_signals', process_group=0)

On Python 3.10 and older you can use preexec_fn:

import os
from subprocess import Popen

def preexec(): # Don't forward signals.
    os.setpgrp()

Popen('do_not_want_signals', preexec_fn=preexec)
# The above can be shortened to:
Popen('do_not_want_signals', preexec_fn=os.setpgrp)
Marek Sapota
  • 20,103
  • 3
  • 34
  • 47
  • 14
    +1 You don't need the `preexec` function, `Popen(args, preexec_nf=os.setpgrp)` is cool too. – Peter Sutton Nov 06 '14 at 21:59
  • 8
    preexec_nf? Better try `Popen(args, preexec_fn=os.setpgrp)` ;-) – Marcus Jan 24 '17 at 12:00
  • 1
    On Python 3.11+ get rid of `preexec_fn=os.setpgrp` in favor of `process_group=0` as preexec_fn is incompatible with threads. On earlier Python versions you can set `start_new_session=True` and achieve a similar result rather than that `preexec_fn=`, though it's `setsid()` call also does more... – gps Mar 06 '23 at 23:51
9

you can do something like this to make it work in windows and unix:

import subprocess
import sys

def pre_exec():
    # To ignore CTRL+C signal in the new process
    signal.signal(signal.SIGINT, signal.SIG_IGN)

if sys.platform.startswith('win'):
    #https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
    #CREATE_NEW_PROCESS_GROUP=0x00000200 -> If this flag is specified, CTRL+C signals will be disabled
    my_sub_process=subprocess.Popen(["executable"], creationflags=0x00000200)
else:
    my_sub_process=subprocess.Popen(["executable"], preexec_fn = pre_exec)
jpastell
  • 91
  • 1
  • 2
  • 2
    When I use your `creationflags`, the main process is unkillable with Ctrl+C on windows. Ideas? – Fuzzyma Nov 22 '16 at 13:00
  • @Fuzzyma i found a quick workaround for this by using `win32api` instead of signal: `win32api.SetConsoleCtrlHandler(exit_handler, True)`. Worked like a charm. – Shameer Kashif Aug 18 '21 at 13:37
  • similar note as on the other answer, avoid `preexec_fn=` and use 3.11's `process_group=0` or 3.2's `start_new_session=True` flags as `preexec_fn=` is not thread safe. – gps Mar 06 '23 at 23:54
3

After an hour of various attempts, this works for me:

process = subprocess.Popen(["someprocess"], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP)

It's solution for windows.

Danil Shaykhutdinov
  • 2,027
  • 21
  • 26
1

Try setting SIGINT to be ignored before spawning the subprocess (reset it to default behavior afterward).

If that doesn't work, you'll need to read up on job control and learn how to put a process in its own background process group, so that ^C doesn't even cause the kernel to send the signal to it in the first place. (May not be possible in Python without writing C helpers.)

See also this older question.

Community
  • 1
  • 1
zwol
  • 135,547
  • 38
  • 252
  • 361
  • I tried this, but it did not work (ignoring SIGINT *before* spawning the subprocess, i.e. not the job control thing). I have a workaround for now, which I will present later today or tomorrow. – robert Feb 18 '11 at 20:08