Вопрос или проблема
Мне нужно предотвратить распространение 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, пока выполняются остальные функции. Поскольку обработка отменяется после выполнения основной логики, это позволяет избежать необходимости повторять содержащуюся логику в каждой функции, что делает код более чистым и менее подверженным ошибкам.
Заключение
Таким образом, нормально отрегулированная обработка сигнала является важным аспектом при реализации последовательных шагов в сценариях, где необходимо сохранить контроль после прерывания. Применяемая концепция позволяет эффективно управлять исполнением функций и избежать избыточной обработки сигнала во всех родительских функциях.
Теперь вы можете использовать данную структуру для выполнения других должностных обязанностей, избегая повторного задания обработчиков сигналов в каждой функции.