Por que não consigo eliminar um tempo limite chamado de um script Bash com um pressionamento de tecla?

11

[Editar: Isso é semelhante a algumas outras perguntas perguntando como matar todos os processos gerados - as respostas parecem usar o pkill. Portanto, o núcleo da minha pergunta pode ser: Existe uma maneira de propagar Ctrl-C / Z para todos os processos gerados por um script?]

Ao chamar um SoX reccom o timeoutcomando coreutils (discutido aqui ), não parece haver nenhuma maneira de matá-lo com um pressionamento de tecla depois que ele é chamado a partir de um script Bash.

Exemplos:

timeout 10 rec test.wav

... pode ser morto com Ctrl+ Cou Ctrl+ Zdo bash, mas não quando é chamado de dentro de um script.

timeout 10 ping nowhere

... pode ser morto com Ctrl+ Cou Ctrl+ Zdo bash e com Ctrl+ Zquando executado dentro de um script.

Posso encontrar o ID do processo e matá-lo dessa maneira, mas por que não consigo usar um pressionamento de tecla de quebra padrão? E existe alguma maneira de estruturar meu script para que eu possa?

meetar
fonte
2
Ctrl + Z não mata processos, apenas os pausa. Eles vão continuar correndo se você der o bgde fgcomandos. De qualquer forma, existe alguma diferença entre os exemplos 1 e 3?
terdon
Eu não tenho timeoutno meu sistema, mas matando sleepobras se ele está digitado diretamente na linha de comando, origem, executado ou passaram pelo intérprete explicitamente
Kevin
@terdon Obrigado, esclareci os exemplos.
meetar

Respostas:

18

Teclas de sinal como Ctrl+ Cenviam um sinal para todos os processos no grupo de processos em primeiro plano .

No caso típico, um grupo de processos é um pipeline. Por exemplo, em head <somefile | sort, o processo em execução heade o processo em execução sortestão no mesmo grupo de processos, assim como o shell, para que todos recebam o sinal. Quando você executa um trabalho em segundo plano ( somecommand &), esse trabalho está em seu próprio grupo de processos; portanto, pressionar Ctrl+ Cnão o afeta.

O timeoutprograma se coloca em seu próprio grupo de processos. Do código fonte:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

Quando ocorre um tempo limite, timeoutpassa pelo simples expediente de matar o grupo de processos do qual ele é membro. Como se colocou em um grupo de processos separado, o processo pai não estará no grupo. O uso de um grupo de processos aqui garante que, se o aplicativo filho for dividido em vários processos, todos os seus processos receberão o sinal.

Quando você executa timeoutdiretamente na linha de comando e pressiona Ctrl+ C, o SIGINT resultante é recebido pelo timeoutprocesso filho e por ele, mas não pelo shell interativo, que é timeouto processo pai do processo. Quando timeouté chamado de um script, somente o shell que executa o script recebe o sinal: timeoutnão o recebe, pois está em um grupo de processos diferente.

Você pode definir um manipulador de sinal em um script de shell com o trapbuiltin. Infelizmente, não é assim tão simples. Considere isto:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

Se você pressionar Ctrl+ Capós 2 segundos, isso ainda aguarda os 5 segundos completos e imprima a mensagem “Interrompido”. Isso ocorre porque o shell evita executar o código de interceptação enquanto um trabalho em primeiro plano está ativo.

Para remediar isso, execute o trabalho em segundo plano. No manipulador de sinal, chame killpara retransmitir o sinal para o timeoutgrupo de processos.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Gilles 'SO- parar de ser mau'
fonte
Muito sorrateira - funcionou lindamente! Eu sou muito mais esperto agora, merci!
meetar
13

Com base na excelente resposta fornecida por Gilles. O comando timeout tem uma opção em primeiro plano, se usado, um CTRL + C sairá do comando timeout.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Marwan Alsabbagh
fonte
Esta é uma ótima resposta - mais simples que a resposta aceita e funcionou bem para mim, usando o timeoutGNU coreutils.
RichVel
1

Basicamente Ctrl+ Cenvia um SIGINTsinal, enquanto Ctrl+ Z envia um SIGTSTPsinal.

SIGTSTPapenas interrompe o processo, a SIGCONTcontinuará.

Isso funciona no processo de primeiro plano que foi bifurcado na linha de comando.

Se o seu processo for um processo em segundo plano, você precisará enviar esse sinal para os processos de uma maneira diferente. killvai fazer isso. Em teoria, um operador "-" nessa matança também deve sinalizar processos filhos, mas isso raramente funciona como o esperado.

Para leitura adicional: Unix-Signals

Nils
fonte
Isso é verdade, pelo menos até “raramente funciona como esperado” (o que depende de suas expectativas - você deve ler sobre grupos de processos), mas completamente irrelevante. A pergunta não revela nenhuma confusão sobre o SIGINT e o SIGTSTP.
Gilles 'SO- stop be evil'
0

Melhorando a resposta do @Gilles, fornecendo uma abordagem "localizar e substituir" para scripts existentes:

  1. Adicione este snippet no início do seu código:
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. Adicione (ou mescle) o seguinte código ao seu INTmanipulador:
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. Substitua os timeoutcomandos pela my_timeoutfunção em seu script.

Exemplo

Aqui está um exemplo de script:

#!/bin/bash

# see "killing timeout": /unix//a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
ceremcem
fonte