Предотвратить распространение SIGINT от подсистемы к родительской оболочке в Zsh

Вопрос или проблема

Мне нужно предотвратить распространение SIGINT (Ctrl-C) от подсистемы к функциям родительской оболочки в Zsh.

Вот минимальный пример:

function sox-record {
    local output="${1:-$(mktemp).wav}"
    (
        rec "${output}" trim 0 300  # Часть пакета sox
    )
    echo "${output}"  # Нужно, чтобы продолжить выполнение после Ctrl-C
}

function audio-postprocess {
    local audio="$(sox-record)"
    # Обработать аудиофайл...
    echo "${audio}"
}

function audio-transcribe {
    local audio="$(audio-postprocess)"
    # Отправить в службу транскрипции...
    transcribe_audio "${audio}"  # Никогда не достигается, если Ctrl-C во время записи
}

Текущий обходной путь требует перехвата SIGINT на каждом уровне, что приводит к повторяющемуся, подверженному ошибкам коду:

function sox-record {
    local output="${1:-$(mktemp).wav}"
    setopt localtraps
    trap '' INT
    (
        rec "${output}" trim 0 300
    )
    trap - INT
    echo "${output}"
}

function audio-postprocess {
    setopt localtraps
    trap '' INT
    local audio="$(sox-record)"
    trap - INT
    # Обработать аудиофайл...
    echo "${audio}"
}

function audio-transcribe {
    setopt localtraps
    trap '' INT
    local audio="$(audio-postprocess)"
    trap - INT
    # Отправить в службу транскрипции...
    transcribe_audio "${audio}"
}

Когда пользователь нажимает Ctrl-C, чтобы остановить запись, я хочу: 1. Чтобы подсистема rec завершилась (работает) 2. Чтобы родительские функции продолжали выполняться (для этого требуется перехват SIGINT в каждом вызове)

Я знаю, что:

  • SIGINT отправляется всем процессам в группе процессов переднего плана
  • Использование setsid создает новую группу процессов, но предотвращает поступление сигналов в дочерний процесс
  • Добавление trap '' INT в родительскую функцию требует, чтобы все вызывающие функции также перехватывали SIGINT, чтобы предотвратить его распространение

Существует ли способ изолировать SIGINT только для подсистемы без необходимости обработки сигналов во всех родительских функциях? Или это принципиально невозможно из-за того, как работают группы процессов и распространение сигналов в Unix?


Я посмотрел на этот вопрос, и попробовал это:

function sox-record {
    local output="${1:-$(mktemp).wav}"

    zsh -mfc "rec "${output}" trim 0 300" </dev/null >&2 || true

    echo "${output}"
}

Хотя это работает, когда я просто вызываю sox-record, когда я вызываю родительскую функцию, такую как audio-postprocess, Ctrl-C ничего не делает. (И мне нужно использовать pkill, чтобы убить rec.)

function audio-postprocess {
    local audio="$(sox-record)"

    # Обработать аудиофайл...
    echo "${audio}"
}

SIGINT не распространяется на родителей. При нажатии ^C ядро отправляет SIGINT на группу процессов переднего плана терминала.

Когда этот скрипт zsh запускается в терминале, ваша интерактивная оболочка создает для него группу процессов (задачу), делает ее группой процессов переднего плана терминала и выполняет скрипт. Все процессы, запущенные этим скриптом, включая подсистемы и тот, который выполняет rec, будут в этой группе и получат SIGINT при ^C.

Если вы хотите, чтобы только процесс, запущенный rec, получил этот сигнал, игнорируйте SIGINT глобально на верхнем уровне с помощью trap '' INT и восстанавливайте его только для rec с (trap - INT; rec...).

function transcribe_audio {
    print -r Would transcribe $1
}
function sox-record {
    local output="${1-$(mktemp).wav}"
    (trap - INT; rec -- "$output" trim 0 300)
    print -r -- "$output"
}
function audio-postprocess {
    local audio="$(sox-record)"
    # Обработать аудиофайл...
    print -r -- "$audio"
}
function audio-transcribe {
    local audio="$(audio-postprocess)"
    transcribe_audio "$audio"
}
trap '' INT
audio-transcribe
trap - INT

# остальная часть скрипта не защищена от SIGINT

Вы также можете переместить trap внутрь audio-transcribe, но вам нужно помнить, чтобы не выполнять его в подсистеме, иначе игнорирование SIGINT будет применяться только к этой подсистеме и ее потомкам, но не к родительскому процессу.

...
function audio-transcribe {
    trap '' INT
    local audio="$(audio-postprocess)"
    transcribe_audio "$audio"
    trap - INT
}
audio-transcribe         # ОК
(audio-transcribe)       # не ОК
blah=$(audio-transcribe) # не ОК
audio-transcribe | blah  # не ОК

Заставить скрипт выполнять управление задачами самостоятельно, то есть поместить rec в свою собственную группу процессов и сделать эту группу процессов группой процессов переднего плана, — это подход, который также может сработать, но в zsh (по крайней мере, 5.9) вы не можете сделать это только с помощью опции monitor (установленной с помощью set -m), поскольку в неинтерактивных вызовах она создает группы процессов для команд, но не меняет группу процессов терминала переднего плана, так что это имело бы противоположный эффект от желаемого.

Чтобы zsh мог выполнять управление задачами терминала, вам нужно использовать interactive (-i) вместо этого.

$ zsh -fc 'ps -o pid,pgid,tpgid,args; exit'
    PID    PGID   TPGID COMMAND
  32962   32962   32977 /bin/zsh
  32977   32977   32977 zsh -fc ps -o pid,pgid,tpgid,args; exit
  32978   32977   32977 ps -o pid,pgid,tpgid,args

Ни -i, ни -m, ps в одной и той же группе процессов, что и родитель, и в переднем плане.

$ zsh -m -fc 'ps -o pid,pgid,tpgid,args; exit'
    PID    PGID   TPGID COMMAND
  32962   32962   32987 /bin/zsh
  32987   32987   32987 zsh -m -fc ps -o pid,pgid,tpgid,args; exit
  32988   32988   32987 ps -o pid,pgid,tpgid,args

С -m в одиночку ps находится в новой группе процессов, но это не было сделано группой процессов переднего плана терминала (tpgid), поэтому фактически оно находится в фоновом режиме и не получит SIGINT при ^C.

$ zsh -i -fc 'ps -o pid,pgid,tpgid,args; exit'
    PID    PGID   TPGID COMMAND
  32962   32962   32997 /bin/zsh
  32996   32996   32997 zsh -i -fc ps -o pid,pgid,tpgid,args; exit
  32997   32997   32997 ps -o pid,pgid,tpgid,args

С -i, который подразумевает -m, ps находится в новой группе процессов, которая на этот раз находится в переднем плане, в то время как zsh сама в фоновом режиме.

Играя с управлением задачами в скриптах, как правило, является плохой идеей, так как это является источником всех видов неприятного неожиданного поведения, поэтому я бы не стал рисковать. Если вы все же решите это сделать, убедитесь, что вы пишете это так:

zsh -ifc 'rec -- "$1" trim 0 300' zsh "$output"

Не встраивайте расширение $output в код zsh, так как это создаст уязвимость для инъекций команд.

Ответ или решение

На вопрос о том, как предотвратить распространение сигнала SIGINT (нажатием Ctrl-C) от подсистемы к родительской оболочке в Zsh, можно ответить достаточно подробно. Проблема заключается в том, что при нажатии Ctrl-C сигнал SIGINT отправляется всем процессам в группе процессов переднего плана терминала, что включает в себя и родительские процессы.

1. Основная проблема

Ваша работа с функциями определяет, что необходимо завершить запись аудиофайла в подсистеме, но при этом важно, чтобы родительские функции продолжили выполнение. Стандартное поведение оболочки Zsh на это нажатие сигнала подразумевает исключение процесса, который выполняет команду записи. Таким образом, если вы не обернете обработку сигнала, SIGINT дойдет до всех процессов в группе.

2. Решение проблемы

Одним из правильных подходов будет использование управления сигналами и организация обработки SIGINT именно в той подсистеме, где запускается команда rec. Ниже приведен пример реализации такого решения с использованием trap для блокировки SIGINT только во время выполнения команды записи:

function sox-record {
    local output="${1:-$(mktemp --suffix=.wav)}"
    (
        trap '' INT  # Игнорируем сигнал в подсистеме
        rec -- "$output" trim 0 300  # Запускаем команду записи
    )
    echo "$output"  # Возвращаем результат
}

function audio-postprocess {
    local audio="$(sox-record)"
    # Здесь можно добавить обработку аудиофайла...
    echo "$audio"
}

function audio-transcribe {
    local audio="$(audio-postprocess)"
    transcribe_audio "$audio"  # Отправляем на транскрипцию
}

# Глобальный блокировщик сигнала на уровне главного процесса
trap '' INT
audio-transcribe  # Запускаем функции
trap - INT  # Восстанавливаем обработчик сигнала

3. Объяснение решения

  • В функции sox-record добавляется локальный обработчик сигнала. Он игнорирует SIGINT, когда выполняется команда rec.
  • Далее в конце функции возвращается имя созданного аудиофайла, что гарантирует, что даже при нажатии Ctrl-C команда rec будет остановлена, а родительские функции продолжат выполнение.
  • Использование trap '' INT на уровне главного процесса предотвращает моментальное получение SIGINT, пока выполняются остальные функции. Поскольку обработка отменяется после выполнения основной логики, это позволяет избежать необходимости повторять содержащуюся логику в каждой функции, что делает код более чистым и менее подверженным ошибкам.

Заключение

Таким образом, нормально отрегулированная обработка сигнала является важным аспектом при реализации последовательных шагов в сценариях, где необходимо сохранить контроль после прерывания. Применяемая концепция позволяет эффективно управлять исполнением функций и избежать избыточной обработки сигнала во всех родительских функциях.

Теперь вы можете использовать данную структуру для выполнения других должностных обязанностей, избегая повторного задания обработчиков сигналов в каждой функции.

Оцените материал
Добавить комментарий

Капча загружается...