Как остановить python от распространения сигналов на подпроцессы?

Я использую python для управления некоторыми симуляторами. Я создаю параметры и запускаю программу, используя:

pipe = open('/dev/null', 'w')
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)

Мой код обрабатывает разные сигналы. Ctrl + C остановит симуляцию, спросит, хочу ли я сохранить и выйдет изящно. У меня есть другие обработчики сигналов (например, для принудительного вывода данных).

Я хочу отправить сигнал (SIGINT, Ctrl + C) на мой python script, который спросит пользователя, какой сигнал он хочет отправить в программу.

Единственное, что предотвращает работу кода, это то, что кажется, что что бы я ни делал, Ctrl + C будет "перенаправлен" на подпроцесс: код поймает его и выйдет:

try:
  <wait for available slots>
except KeyboardInterrupt:
  print "KeyboardInterrupt catched! All simulations are paused. Please choose the signal to send:"
  print "  0: SIGCONT (Continue simulation)"
  print "  1: SIGINT  (Exit and save)"
  [...]
  answer = raw_input()
  pid.send_signal(signal.SIGCONT)
  if   (answer == "0"):
    print "    --> Continuing simulation..."
  elif (answer == "1"):
    print "    --> Exit and save."
    pid.send_signal(signal.SIGINT)
    [...]

Итак, что бы я ни делал, программа получает SIGINT, который мне нужен только для моего python script. Как я могу это сделать?

Я также пробовал:

signal.signal(signal.SIGINT, signal.SIG_IGN)
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)
signal.signal(signal.SIGINT, signal.SIG_DFL)

для запуска программы, но это дает тот же результат: программа ловит SIGINT.

Thanx!

Ответ 1

Сочетание некоторых других ответов, которые будут делать трюк - ни один сигнал, отправленный в основное приложение, не будет перенаправлен на подпроцесс.

import os
from subprocess import Popen

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

Popen('whatever', preexec_fn = preexec)

Ответ 2

Это действительно можно сделать, используя ctypes. Я бы не рекомендовал это решение, но я был заинтересован в том, чтобы что-то приготовить, поэтому я решил поделиться им.

parent.py

#!/usr/bin/python

from ctypes import *
import signal
import subprocess
import sys
import time

# Get the size of the array used to
# represent the signal mask
SIGSET_NWORDS = 1024 / (8 * sizeof(c_ulong))

# Define the sigset_t structure
class SIGSET(Structure):
    _fields_ = [
        ('val', c_ulong * SIGSET_NWORDS)
    ]

# Create a new sigset_t to mask out SIGINT
sigs = (c_ulong * SIGSET_NWORDS)()
sigs[0] = 2 ** (signal.SIGINT - 1)
mask = SIGSET(sigs)

libc = CDLL('libc.so.6')

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from parent!")

def disable_sig():
    '''Mask the SIGINT in the child process'''
    SIG_BLOCK = 0
    libc.sigprocmask(SIG_BLOCK, pointer(mask), 0)

# Set up the parent signal handler
signal.signal(signal.SIGINT, handle)

# Call the child process
pid = subprocess.Popen("./child.py", stdout=sys.stdout, stderr=sys.stdin, preexec_fn=disable_sig)

while (1):
    time.sleep(1)

child.py

#!/usr/bin/python
import time
import signal

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from child!")

signal.signal(signal.SIGINT, handle)
while (1):
    time.sleep(1)

Обратите внимание, что это делает кучу допущений о различных структурах libc и как таковых, вероятно, довольно хрупким. При запуске вы не увидите сообщение "SIGINT от ребенка!". распечатаны. Однако, если вы закомментируете вызов sigprocmask, тогда вы это сделаете. Кажется, чтобы выполнить эту работу:)

Ответ 3

POSIX говорит, что программа, выполняемая с execvp (это то, что использует подпроцесс .Popen), должно наследовать маску сигнала вызывающего процесса.

Я мог ошибаться, но я не думаю, что вызов signal изменяет маску. Вы хотите sigprocmask, который python напрямую не раскрывает.

Это будет взлом, но вы можете попробовать установить его через прямой вызов libc через ctypes. Я определенно был бы заинтересован в том, чтобы лучше ответить на эту стратегию.

Альтернативной стратегией будет опрос stdin для ввода пользователем как часть вашего основного цикла. ( "Нажмите Q, чтобы выйти/приостановить" - что-то вроде этого.) Это оборачивается проблемой обработки сигналов.

Ответ 4

Я решил эту проблему, создав вспомогательное приложение, которое я вызываю, вместо того, чтобы напрямую создавать дочерний элемент. Этот помощник меняет родительскую группу, а затем порождает реальный дочерний процесс.

import os
import sys

from time import sleep
from subprocess import Popen

POLL_INTERVAL=2

# dettach from parent group (no more inherited signals!)
os.setpgrp()

app = Popen(sys.argv[1:])
while app.poll() is None:
    sleep(POLL_INTERVAL)

exit(app.returncode)

Я вызываю этот помощник в родительском, передавая реальный ребенок и его параметры в качестве аргументов:

Popen(["helper", "child", "arg1", ...])

Мне нужно сделать это, потому что мое дочернее приложение не находится под моим контролем, если бы я мог добавить setpgrp туда и вообще обходить помощника.

Ответ 5

Функция:

os.setpgrp()

Хорошо работает, только если Popen вызывается сразу после. Если вы пытаетесь предотвратить распространение сигналов на подпроцессы произвольного пакета, пакет может переопределить это, прежде чем создавать подпроцессы, вызывающие распространение сигналов в любом случае. Это тот случай, когда, например, пытается предотвратить распространение сигнала в процессах веб-браузера, порожденных из пакета Selenium.

Эта функция также удаляет возможность легко общаться между отдельными процессами без каких-либо сокетов.

Для моих целей это казалось излишним. Вместо того, чтобы беспокоиться о распространении сигналов, я использовал собственный сигнал SIGUSR1. Многие пакеты Python игнорируют SIGUSR1, поэтому, даже если они отправляются во все подпроцессы, их обычно игнорируют.

Он может быть отправлен процессу в bash на Ubuntu с помощью

kill -10 <pid>

Он может быть распознан в вашем коде через

signal.signal(signal.SIGUSR1, callback_function)

Доступные номера сигналов на Ubuntu можно найти в /usr/include/asm/signal.h.