Как отправить SIGINT на Python из bash-скрипта?

Я хочу запустить фоновое задание Python из bash-скрипта, а затем аккуратно убить его с помощью SIGINT. Это прекрасно работает из оболочки, но я не могу заставить его работать в сценарии.

loop.py:

#! /usr/bin/env python
if __name__ == "__main__":
    try:
        print 'starting loop'
        while True:
            pass
    except KeyboardInterrupt:
        print 'quitting loop'

Из оболочки я могу его прервать:

$ python loop.py &
[1] 15420
starting loop
$ kill -SIGINT 15420
quitting loop
[1]+  Done                    python loop.py

kill.sh:

#! /bin/bash
python loop.py &
PID=$!
echo "sending SIGINT to process $PID"
kill -SIGINT $PID

Но из сценария я не могу:

$ ./kill.sh 
starting loop
sending SIGINT to process 15452
$ ps ax | grep loop.py | grep -v grep
15452 pts/3    R      0:08 python loop.py

И, если он был запущен из скрипта, я больше не могу убить его из оболочки:

$ kill -SIGINT 15452
$ ps ax | grep loop.py | grep -v grep
15452 pts/3    R      0:34 python loop.py

Я предполагаю, что мне не хватает какой-то тонкой точки контроля работы Bash.

5 ответов

Решение

Вы не регистрируете обработчик сигнала. Попробуйте ниже. Кажется, работает достаточно надежно. Я думаю, что редкое исключение - когда он ловит сигнал, прежде чем Python регистрирует обработчик скрипта. Обратите внимание, что KeyboardInterrupt должен вызываться только "когда пользователь нажимает клавишу прерывания". Я думаю, что тот факт, что он работает для явного (например, через kill) SIGINT, является случайностью реализации.

import signal

def quit_gracefully(*args):
    print 'quitting loop'
    exit(0);

if __name__ == "__main__":
    signal.signal(signal.SIGINT, quit_gracefully)

    try:
        print 'starting loop'
        while True:
            pass
    except KeyboardInterrupt:
        quit_gracefully()

В дополнение к ответу @matthew-flaschen вы можете использовать exec в скрипте bash для эффективной замены области видимости открываемого процесса:

#!/bin/bash
exec python loop.py &
PID=$!
sleep 5  # waiting for the python process to come up

echo "sending SIGINT to process $PID"
kill -SIGINT $PID

Я согласен с Мэтью Флашеном; проблема в python, который, очевидно, не регистрирует исключение KeyboardInterrupt в SIGINT, когда он не вызывается из интерактивной оболочки.

Конечно, ничто не мешает вам зарегистрировать ваш обработчик сигналов следующим образом:

def signal_handler(signum, frame):
    raise KeyboardInterrupt, "Signal handler"

Когда вы запускаете команду в фоновом режиме с &, SIGINT будет игнорироваться. Вот соответствующий раздел man bash:

У не встроенных команд, запускаемых bash, в обработчиках сигналов установлены значения, унаследованные оболочкой от ее родителя. Когда управление заданиями не действует, асинхронные команды игнорируют SIGINT и SIGQUIT в дополнение к этим унаследованным обработчикам. Команды, выполняемые в результате подстановки команд, игнорируют генерируемые клавиатурой сигналы управления заданиями SIGTTIN, SIGTTOU и SIGTSTP.

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

В скрипте kill.sh тоже есть проблема. Поскольку loop.py отправляется в фоновый режим, нет гарантии, что kill выполняется после python loop.py.

#! /bin/bash
python loop.py &
PID=$!
#
# NEED TO WAIT ON EXISTENCE OF python loop.py PROCESS HERE.
#
echo "sending SIGINT to process $PID"
kill -SIGINT $PID

Пробовал подход @Steen, но, увы, он явно не держится на Mac.

Другое решение, почти то же самое, что и выше, но немного более общее, - просто переустановить обработчик по умолчанию, если SIGINT игнорируется:

def _ensure_sigint_handler():
    # On Mac, even using `exec <cmd>` in `bash` still yields an ignored SIGINT.
    sig = signal.getsignal(signal.SIGINT)
    if signal.getsignal(signal.SIGINT) == signal.SIG_IGN:
        signal.signal(signal.SIGINT, signal.default_int_handler)
# ...
_ensure_sigint_handler()
Другие вопросы по тегам