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
- Sempre usar nomes de sinais (não números) para portabilidade entre sistemas
- Combinar traps com
set -eeset -upara 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
- Bash Manual: Signals and Traps — Documentação oficial do Bash sobre sinais e o comando trap
- Advanced Bash-Scripting Guide: Signal Handling — Guia avançado com exemplos detalhados de tratamento de sinais
- Linux man page: signal(7) — Página de manual completa sobre sinais no Linux
- Shell Scripting Tutorial: Traps — Tutorial prático sobre uso de traps em scripts shell
- Bash Hackers Wiki: Signal handling — Wiki da comunidade com exemplos e boas práticas
- IBM Developer: Signal handling in shell scripts — Tutorial da IBM sobre tratamento de sinais em scripts shell