Signal handling em scripts longos

1. Fundamentos de Sinais no Bash

Sinais são notificações assíncronas que o kernel envia a processos para comunicar eventos como interrupções do usuário, término solicitado ou condições de erro. Quando um shell script está em execução, ele recebe sinais como qualquer outro processo.

Os sinais mais relevantes para scripts longos são:

  • SIGINT (2): Enviado quando o usuário pressiona Ctrl+C. Comportamento padrão: termina o processo.
  • SIGTERM (15): Solicitação educada de término. Comportamento padrão: termina o processo.
  • SIGHUP (1): Tradicionalmente enviado quando o terminal é fechado. Comportamento padrão: termina o processo.
  • SIGQUIT (3): Similar ao SIGINT, mas gera core dump. Enviado com Ctrl+.

Sem tratamento customizado, o comportamento padrão encerra o script abruptamente, sem chance de limpeza. Para scripts longos que criam arquivos temporários, alteram estado do sistema ou processam dados em lote, isso pode ser catastrófico.

2. Capturando Sinais com trap

A sintaxe básica do trap é:

trap 'comando' SIGNAME
#!/bin/bash
trap 'echo "Sinal SIGINT recebido"' INT
echo "Pressione Ctrl+C para testar"
sleep 30

Armadilha com aspas: Quando usamos aspas simples, a expansão de variáveis ocorre no momento da execução do trap. Com aspas duplas, a expansão ocorre no momento da definição.

# Expansão no momento da execução (recomendado)
trap 'echo "Arquivo: $temp_file"' INT

# Expansão no momento da definição (provavelmente indesejado)
trap "echo 'Arquivo: $temp_file'" INT

Múltiplos sinais em um único trap:

cleanup() {
    rm -f /tmp/meu_script_*
    echo "Limpeza concluída"
}
trap cleanup INT TERM EXIT

O sinal EXIT é especial: ele é executado sempre que o script termina, seja por sucesso, erro ou sinal externo.

3. Limpeza Graciosa em Scripts Longos

Scripts longos frequentemente criam recursos que precisam ser liberados. Uma função de cleanup bem estruturada é essencial.

#!/bin/bash

# Variáveis globais para estado
ORIGINAL_DIR=$(pwd)
TEMP_DIR=$(mktemp -d)
PROGRESS_FILE="/tmp/progress_$$.txt"

cleanup() {
    local exit_code=$?
    echo "[$(date)] Iniciando limpeza (código de saída: $exit_code)"

    # Remover arquivos temporários
    rm -rf "$TEMP_DIR"
    rm -f "$PROGRESS_FILE"

    # Restaurar diretório original
    cd "$ORIGINAL_DIR" 2>/dev/null

    echo "[$(date)] Limpeza concluída"
    exit $exit_code
}

trap cleanup INT TERM EXIT

# Corpo do script longo
cd "$TEMP_DIR" || exit 1
echo "Trabalhando em $TEMP_DIR"
# ... processamento longo ...

4. Tratamento Específico para Scripts em Loop

Loops longos exigem cuidado especial. Um sinal pode chegar no meio de uma iteração, corrompendo dados.

Usando flags de controle:

#!/bin/bash

STOP_REQUESTED=0

handle_signal() {
    echo "[$(date)] Sinal recebido. Finalizando após iteração atual..."
    STOP_REQUESTED=1
}

trap handle_signal INT TERM

for i in {1..1000}; do
    if [ $STOP_REQUESTED -eq 1 ]; then
        echo "Parada solicitada na iteração $i"
        break
    fi

    # Processamento da iteração
    echo "Iteração $i"
    sleep 1
done

echo "Script finalizado"

Salvando checkpoints:

#!/bin/bash

CHECKPOINT_FILE="/tmp/checkpoint.txt"
CURRENT_ITEM=0

save_checkpoint() {
    echo "$CURRENT_ITEM" > "$CHECKPOINT_FILE"
}

trap 'save_checkpoint; echo "Checkpoint salvo em $CURRENT_ITEM"; exit' INT TERM

# Simulando processamento de 100 itens
for ((CURRENT_ITEM=1; CURRENT_ITEM<=100; CURRENT_ITEM++)); do
    echo "Processando item $CURRENT_ITEM"
    sleep 2
    save_checkpoint
done

5. Ignorando e Resetando Sinais

Em algumas situações, ignorar sinais é necessário — por exemplo, durante uma operação crítica que não pode ser interrompida.

Ignorando sinais deliberadamente:

#!/bin/bash

echo "Fase crítica: não pode ser interrompida"
trap '' INT TERM
sleep 10
echo "Fase crítica concluída"

# Restaurando handler padrão
trap - INT TERM
echo "Agora você pode interromper"
sleep 10

Cuidado com subshells: Subshells herdam os traps do shell pai, mas modificações em subshells não afetam o pai.

#!/bin/bash
trap 'echo "Pai: INT recebido"' INT

# Subshell - herda o trap mas não afeta o pai
(
    trap 'echo "Subshell: INT recebido"' INT
    sleep 5
) &

sleep 10

6. Sinais Customizados e Comunicação entre Processos

Sinais do usuário (SIGUSR1 e SIGUSR2) permitem comunicação simples entre processos.

Reload de configuração:

#!/bin/bash

CONFIG_FILE="/tmp/config.txt"
RELOAD_REQUESTED=0

reload_config() {
    echo "[$(date)] Recarregando configuração..."
    source "$CONFIG_FILE" 2>/dev/null || echo "Erro ao carregar config"
    RELOAD_REQUESTED=1
}

trap reload_config USR1

echo "PID do script: $$"
echo "Envie: kill -USR1 $$ para recarregar configuração"

while true; do
    if [ $RELOAD_REQUESTED -eq 1 ]; then
        echo "Configuração recarregada em $(date)"
        RELOAD_REQUESTED=0
    fi
    echo "Processando... (PID: $$)"
    sleep 5
done

Sincronização entre script pai e filho:

#!/bin/bash

trap 'echo "Filho finalizou"; CHILD_DONE=1' USR1

CHILD_DONE=0

# Iniciar processo filho
(
    sleep 3
    kill -USR1 $$  # Notifica o pai
) &

echo "Aguardando filho..."
while [ $CHILD_DONE -eq 0 ]; do
    sleep 1
done
echo "Filho concluído, continuando..."

7. Debugging de Problemas com Sinais

Logando quando um sinal é recebido:

#!/bin/bash

LOG_FILE="/tmp/signal_debug.log"

log_signal() {
    local signal=$1
    echo "[$(date)] Sinal $signal recebido. PID: $$, PPID: $PPID" >> "$LOG_FILE"
    echo "Contexto: diretório=$(pwd), usuário=$(whoami)" >> "$LOG_FILE"
}

trap 'log_signal INT; exit' INT
trap 'log_signal TERM; exit' TERM

# Script longo
while true; do
    echo "Trabalhando... (PID: $$)"
    sleep 2
done

Testando handlers sem matar o processo:

#!/bin/bash

handle_signal() {
    echo "Handler executado com sucesso"
}

trap handle_signal INT

# Testar se o handler está configurado
kill -0 $$ 2>/dev/null && echo "Processo $$ está vivo"

# Simular sinal sem realmente enviá-lo
echo "Teste de handler concluído"

Evitando race conditions: Nunca assuma que o estado do script é consistente dentro de um handler. Use flags atômicas e minimize operações no handler.

#!/bin/bash

# Evite isso (race condition possível)
trap 'rm -f "$TEMP_FILE"; echo "Limpando"' INT

# Prefira isso
CLEANUP_NEEDED=1
trap 'if [ $CLEANUP_NEEDED -eq 1 ]; then rm -f "$TEMP_FILE"; CLEANUP_NEEDED=0; fi' INT

8. Boas Práticas e Padrões Recomendados

  1. Sempre usar nomes de sinais (não números) para portabilidade entre sistemas
  2. Combinar traps com set -e e set -u para segurança

Exemplo completo de script longo com tratamento robusto:

#!/bin/bash
set -euo pipefail

# Configuração
SCRIPT_NAME=$(basename "$0")
TEMP_DIR=$(mktemp -d)
ORIGINAL_DIR=$(pwd)
STOP_REQUESTED=0
PROGRESS=0

# Função de cleanup
cleanup() {
    local exit_code=$?
    echo "[$(date)] $SCRIPT_NAME: Iniciando limpeza..."
    cd "$ORIGINAL_DIR" 2>/dev/null || true
    rm -rf "$TEMP_DIR" 2>/dev/null || true
    echo "[$(date)] $SCRIPT_NAME: Limpeza concluída (código: $exit_code)"
    exit $exit_code
}

# Handler para sinais
handle_signal() {
    local signal=$1
    echo "[$(date)] $SCRIPT_NAME: Sinal $signal recebido"
    STOP_REQUESTED=1
}

# Configurar traps
trap cleanup EXIT
trap 'handle_signal INT' INT
trap 'handle_signal TERM' TERM
trap 'handle_signal HUP' HUP

# Função para salvar progresso
save_progress() {
    echo "$PROGRESS" > "$TEMP_DIR/progress.txt"
}

# Corpo principal do script
echo "$SCRIPT_NAME iniciado em $(date)"
echo "Diretório temporário: $TEMP_DIR"

for PROGRESS in $(seq 1 100); do
    if [ $STOP_REQUESTED -eq 1 ]; then
        echo "Parada solicitada. Progresso: $PROGRESS%"
        save_progress
        exit 0
    fi

    echo "Processando: $PROGRESS%"
    save_progress
    sleep 1
done

echo "Processamento concluído com sucesso"

Este padrão garante que:
- Arquivos temporários são sempre removidos
- O progresso é salvo para retomada
- Sinais são tratados de forma consistente
- O script se comporta bem em pipelines e ambientes automatizados

Referências