trap Komutu ile Bash Script’lerinde Sinyal Yakalama ve Temiz Çıkış Yönetimi

Üretim ortamında çalışan bir script’in tam ortasında Ctrl+C’ye bastığınızda ne olur? Geçici dosyalar diskte kalır, yarı yazılmış log’lar orada öylece durur, lock dosyaları bir sonraki çalışmada sizi engeller. Bu kaosun önüne geçmek için trap komutu var ve bir kez alışkanlık haline getirdikten sonra onsuz script yazmak mümkün gelmiyor.

trap Nedir ve Neden Önemlidir?

trap, bash’in sinyal ve olay yakalama mekanizmasıdır. Bir process’e sinyal gönderildiğinde veya script belirli noktalara ulaştığında ne yapılacağını önceden tanımlarsınız. Bunu bir tür “son istek” mekanizması gibi düşünebilirsiniz.

Linux’ta her process, kernel tarafından çeşitli sinyaller alabilir. SIGINT (klavyeden Ctrl+C), SIGTERM (kill komutu), SIGHUP (terminal kapandığında) bunların en yaygınları. Normalde bu sinyaller geldiğinde bash script’iniz anında durur ve temizlik yapılmaz. trap ile bu sinyalleri yakalayıp istediğiniz kodun çalışmasını sağlayabilirsiniz.

Temel sözdizimi şu şekildedir:

trap 'komut_veya_fonksiyon' SINYAL [SINYAL2 ...]

Temel Kullanım: İlk trap Deneyimi

En basit örneğe bakalım. Geçici dosya oluşturan ve temizlenmesi gereken bir script:

#!/bin/bash

TEMP_FILE=$(mktemp /tmp/islem_XXXXXX)

cleanup() {
    echo "Temizlik yapılıyor..."
    rm -f "$TEMP_FILE"
    echo "Geçici dosya silindi: $TEMP_FILE"
}

trap cleanup EXIT

echo "Script çalışıyor, geçici dosya: $TEMP_FILE"
echo "Buraya önemli veriler yazılıyor..." > "$TEMP_FILE"

# Simüle edilmiş uzun işlem
sleep 30
echo "İşlem tamamlandı."

Burada EXIT özel bir pseudo-sinyal. Script nasıl çıkarsa çıksın, normal sona erme, hata, Ctrl+C fark etmeksizin cleanup fonksiyonu çalışır. Bu tek satır, script güvenilirliğinizi ciddi ölçüde artırır.

Yaygın Sinyaller ve Anlamları

Sinyalleri teker teker anlamak, doğru kullanım için şart:

  • EXIT: Script herhangi bir şekilde sonlandığında tetiklenir. Pseudo-sinyal, gerçek bir OS sinyali değil
  • SIGINT (2): Ctrl+C ile üretilir. Kullanıcı scripti kesmek istediğinde
  • SIGTERM (15): kill PID ile gönderilir. Sistemin script’i nazikçe durdurma isteği
  • SIGHUP (1): Terminal kapandığında veya nohup olmadan arka planda çalışırken
  • SIGKILL (9): kill -9 ile gönderilir. YAKALANAMAZ. trap ile engellenemez
  • SIGQUIT (3): Ctrl+\ ile üretilir. Core dump oluşturur
  • ERR: Her hata komutundan sonra tetiklenir (set -e ile kombinasyonu dikkat ister)
  • DEBUG: Her komuttan önce tetiklenir, debugging için kullanışlı

Birden Fazla Sinyal Yakalama

Gerçek dünyada genellikle birden fazla sinyali ele almanız gerekir:

#!/bin/bash

LOG_FILE="/var/log/myapp/process.log"
LOCK_FILE="/var/run/myapp.lock"
TEMP_DIR=$(mktemp -d /tmp/myapp_XXXXXX)

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

cleanup() {
    local exit_code=$?
    log "Sinyal alındı, temizlik başlıyor..."
    
    # Geçici dizini temizle
    if [ -d "$TEMP_DIR" ]; then
        rm -rf "$TEMP_DIR"
        log "Geçici dizin temizlendi: $TEMP_DIR"
    fi
    
    # Lock dosyasını kaldır
    if [ -f "$LOCK_FILE" ]; then
        rm -f "$LOCK_FILE"
        log "Lock dosyası kaldırıldı"
    fi
    
    log "Script sonlandı (exit code: $exit_code)"
    exit $exit_code
}

trap cleanup EXIT SIGINT SIGTERM SIGHUP

# Lock dosyası oluştur
echo $$ > "$LOCK_FILE"
log "Script başladı (PID: $$)"

# Asıl iş burada yapılır
for i in $(seq 1 10); do
    log "Adım $i işleniyor..."
    sleep 5
done

Burada $? exit code’unu yakalamak önemli. cleanup fonksiyonu çağrıldığında son komutun exit code’u hâlâ $? içinde. Bu değeri saklayıp en sonda geri dönüyoruz, böylece script’in gerçek exit code’u korunuyor.

trap ile Lock Dosyası Yönetimi

Cron job’larda çakışmayı önlemek için lock mekanizması şart. trap olmadan lock dosyaları hayalet gibi ortalıkta kalır:

#!/bin/bash

LOCK_FILE="/var/run/veri_senkronizasyon.lock"
SCRIPT_NAME=$(basename "$0")

acquire_lock() {
    if [ -f "$LOCK_FILE" ]; then
        local eski_pid
        eski_pid=$(cat "$LOCK_FILE" 2>/dev/null)
        
        if kill -0 "$eski_pid" 2>/dev/null; then
            echo "Script zaten çalışıyor (PID: $eski_pid). Çıkılıyor."
            exit 1
        else
            echo "Eski lock dosyası bulundu ama process yok, temizleniyor."
            rm -f "$LOCK_FILE"
        fi
    fi
    
    echo $$ > "$LOCK_FILE"
    echo "Lock alındı (PID: $$)"
}

release_lock() {
    rm -f "$LOCK_FILE"
    echo "Lock serbest bırakıldı"
}

trap release_lock EXIT

acquire_lock

echo "Senkronizasyon başladı..."
# rsync veya benzeri işlemler burada
sleep 20
echo "Senkronizasyon tamamlandı."

Bu pattern özellikle cron’da çalışan script’lerde hayat kurtarır. Sunucu beklenmedik şekilde kapanıp açıldığında bile bir sonraki çalışmada eski PID’in hayatta olup olmadığını kontrol ediyoruz.

ERR Sinyali ile Hata Yönetimi

ERR pseudo-sinyali, hata ayıklama ve üretim script’lerinde son derece güçlü bir araç:

#!/bin/bash

set -euo pipefail

hata_yonetici() {
    local satir_no=$1
    local exit_code=$2
    local komut=$3
    
    echo "HATA: Satır $satir_no'da başarısız oldu" >&2
    echo "Komut: $komut" >&2
    echo "Exit code: $exit_code" >&2
    
    # Slack veya PagerDuty'e bildirim gönderilebilir
    # curl -s -X POST "$WEBHOOK_URL" -d "{"text": "Script hatası: $komut"}"
    
    cleanup
    exit $exit_code
}

cleanup() {
    # Geçici kaynakları temizle
    [ -d "${TEMP_DIR:-}" ] && rm -rf "$TEMP_DIR"
}

trap 'hata_yonetici $LINENO $? "$BASH_COMMAND"' ERR
trap cleanup EXIT

TEMP_DIR=$(mktemp -d)

echo "Yedekleme başladı..."

# Bu komut başarısız olursa hata yöneticisi devreye girer
tar -czf "$TEMP_DIR/yedek.tar.gz" /var/www/html/

# S3'e yükle
aws s3 cp "$TEMP_DIR/yedek.tar.gz" s3://sirket-yedekleri/

echo "Yedekleme tamamlandı."

$BASH_COMMAND değişkeni, hata veren son komutu içerir. $LINENO ise script içindeki satır numarasını verir. Bu ikisini birleştirince hata raporları çok daha anlamlı hale gelir.

Sinyal Yayılımı: Alt Process’leri Yönetmek

trap’ın en sık gözden kaçan konularından biri: trap sadece mevcut shell için geçerlidir, fork’lanan child process’lere otomatik yayılmaz. Uzun süren arka plan işleri başlatıyorsanız bu kritik:

#!/bin/bash

ARKAPLAN_PIDLER=()

cleanup() {
    echo "Temizlik: arka plan process'leri durduruluyor..."
    
    for pid in "${ARKAPLAN_PIDLER[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            echo "Process durduruluyor: $pid"
            kill -SIGTERM "$pid" 2>/dev/null
            
            # SIGTERM'e yanıt vermezse 5 saniye bekle sonra SIGKILL
            local sayac=0
            while kill -0 "$pid" 2>/dev/null && [ $sayac -lt 5 ]; do
                sleep 1
                ((sayac++))
            done
            
            if kill -0 "$pid" 2>/dev/null; then
                echo "Process yanıt vermedi, SIGKILL gönderiliyor: $pid"
                kill -SIGKILL "$pid" 2>/dev/null
            fi
        fi
    done
    
    echo "Temizlik tamamlandı"
}

trap cleanup EXIT SIGINT SIGTERM

# Paralel işlemler başlat
for servis in "web" "cache" "queue"; do
    ./islemci.sh "$servis" &
    pid=$!
    ARKAPLAN_PIDLER+=($pid)
    echo "$servis başlatıldı (PID: $pid)"
done

echo "Tüm servisler başlatıldı, bekleniyor..."
wait
echo "Tüm işlemler tamamlandı."

PID’leri dizide takip edip cleanup’ta hepsini düzenli şekilde sonlandırmak, özellikle paralel işlem yapan script’lerde şart.

Deployment Script’i: Gerçek Dünya Örneği

Şimdiye kadar gördüklerimizi birleştiren, gerçekçi bir deployment script’i:

#!/bin/bash

set -euo pipefail

# Değişkenler
APP_DIR="/var/www/uygulama"
BACKUP_DIR="/var/backups/uygulama"
DEPLOY_USER="deploy"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TEMP_DIR=$(mktemp -d /tmp/deploy_XXXXXX)
ROLLBACK_GEREKLI=false
SERVIS_DURDURULDU=false

log() {
    echo "[$(date '+%H:%M:%S')] $*"
}

hata() {
    log "HATA: $*" >&2
}

rollback() {
    if [ "$ROLLBACK_GEREKLI" = true ]; then
        log "Rollback başlatılıyor..."
        
        local son_yedek
        son_yedek=$(ls -t "$BACKUP_DIR"/app_*.tar.gz 2>/dev/null | head -1)
        
        if [ -n "$son_yedek" ]; then
            tar -xzf "$son_yedek" -C "$APP_DIR" --strip-components=1
            log "Rollback tamamlandı: $son_yedek"
        else
            hata "Rollback için yedek bulunamadı!"
        fi
    fi
}

cleanup() {
    local exit_code=$?
    
    log "Temizlik aşaması..."
    
    # Geçici dizini temizle
    [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
    
    # Servis durdurulduysa ve başarısız olduysa rollback ve yeniden başlat
    if [ "$SERVIS_DURDURULDU" = true ] && [ $exit_code -ne 0 ]; then
        rollback
        log "Servis yeniden başlatılıyor..."
        systemctl start uygulama || hata "Servis başlatılamadı!"
    fi
    
    exit $exit_code
}

trap cleanup EXIT
trap 'hata "İşlem kullanıcı tarafından kesildi"; exit 130' SIGINT
trap 'hata "SIGTERM alındı"; exit 143' SIGTERM

log "Deployment başladı: $TIMESTAMP"

# Mevcut uygulamayı yedekle
log "Yedekleme alınıyor..."
ROLLBACK_GEREKLI=true
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/app_${TIMESTAMP}.tar.gz" -C "$APP_DIR" .

# Servisi durdur
log "Servis durduruluyor..."
systemctl stop uygulama
SERVIS_DURDURULDU=true

# Yeni kodu deploy et
log "Yeni sürüm yükleniyor..."
tar -xzf "/tmp/release_${TIMESTAMP}.tar.gz" -C "$TEMP_DIR"
rsync -av --delete "$TEMP_DIR/" "$APP_DIR/"

# Migration çalıştır
log "Database migration çalıştırılıyor..."
cd "$APP_DIR" && php artisan migrate --force

# Servisi başlat
log "Servis başlatılıyor..."
systemctl start uygulama
SERVIS_DURDURULDU=false

# Health check
sleep 3
if ! curl -sf http://localhost/health > /dev/null; then
    hata "Health check başarısız!"
    exit 1
fi

ROLLBACK_GEREKLI=false
log "Deployment başarıyla tamamlandı!"

Bu örnekte birkaç önemli pattern var. ROLLBACK_GEREKLI ve SERVIS_DURDURULDU flag’leri, cleanup’ın tam olarak nerede başarısız olunduğunu bilmesini sağlıyor. Deployment ortasında Ctrl+C geleseydi bile sistem tutarlı bir durumda kalırdı.

trap’ı Geçici Olarak Devre Dışı Bırakmak

Bazen belirli bir blok için trap’ı askıya almanız gerekir. Bunu trap '' ile yapabilirsiniz:

#!/bin/bash

cleanup() {
    echo "Cleanup çalışıyor..."
    rm -f /tmp/onemli_dosya
}

trap cleanup EXIT SIGINT

echo "Normal çalışma..."

# Bu blok sırasında Ctrl+C'yi yoksay
trap '' SIGINT
echo "Kritik bölge başladı, Ctrl+C devre dışı..."
sleep 10
echo "Kritik bölge bitti"
trap cleanup SIGINT  # Yeniden aktif et

echo "Normal çalışmaya devam..."
sleep 5

Dikkat: SIGINT’i susturmak kullanıcı deneyimini olumsuz etkiler. Sadece gerçekten kesilemeyecek kritik bölümler için kullanın ve süreyi kısa tutun.

DEBUG Trap ile Script Tracing

Geliştirme aşamasında her komuttan önce çalışan DEBUG trap’ı inanılmaz derecede faydalı:

#!/bin/bash

# Sadece DEBUG modunda etkinleştir
if [ "${DEBUG_SCRIPT:-0}" = "1" ]; then
    trap 'echo ">> Satır $LINENO: $BASH_COMMAND"' DEBUG
fi

trap 'echo "Script bitti (exit: $?)"' EXIT

KULLANICI="ahmet"
DIZIN="/home/$KULLANICI"

if [ -d "$DIZIN" ]; then
    echo "Dizin mevcut: $DIZIN"
    ls -la "$DIZIN"
fi

Çalıştırmak için:

DEBUG_SCRIPT=1 ./script.sh

Bu, set -x alternatifinden daha okunabilir çıktı üretir ve koşullu olarak etkinleştirebildiğiniz için production’da log’ları kirletmez.

Sık Yapılan Hatalar

Tecrübelerimden derlediğim kaçınılması gereken durumlar:

  • Cleanup içinde exit code’u kaybetmek: cleanup() başında local exit_code=$? ile saklayın, fonksiyon sonunda exit $exit_code kullanın
  • Cleanup’ın başarısız olması: cleanup fonksiyonu içindeki komutların hata vermesi asıl exit code’u bozabilir. || true ekleyerek devam etmesini sağlayın
  • SIGKILL’i yakalamaya çalışmak: kill -9 yakalanamaz, buna göre tasarım yapın
  • Subshell’lerde trap beklemek: $(komut) içindeki trap’lar parent shell’e yayılmaz
  • trap’ı döngü içinde tanımlamak: trap tanımı global, her iterasyonda yeniden tanımlamak gerek yok ve performansı etkiler

Sonuç

trap, bash script yazmanın olmazsa olmaz parçalarından biri. Basit bir cleanup fonksiyonu ile başlayıp zamanla daha karmaşık sinyal yönetimi senaryolarına geçebilirsiniz.

Benim kişisel yaklaşımım şu: her script’te minimum olarak trap cleanup EXIT kullanmak. Bu tek satır, script’inizin ne zaman durduğundan bağımsız olarak temizlik yapmasını garantiler. Geçici dosyalar, lock dosyaları, database bağlantıları, arka plan process’leri; bunların hepsi cleanup fonksiyonunda ele alınabilir.

Üretim ortamındaki script’lerde ise SIGINT ve SIGTERM’i ayrı ayrı ele alıp uygun exit code’ları döndürmek profesyonel bir yaklaşım. Monitoring sistemleri ve process manager’lar (systemd, supervisor) bu exit code’lara göre karar verir.

Son olarak, trap kullanmak bir güvenlik ağı değil, tasarımın parçası olmalı. Script’inizi “bu aniden durduğunda sistem ne durumda kalır?” sorusunu sorarak yazın ve trap’ı bu sorunun cevabına göre şekillendirin.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir