Skrip yang berhenti mendadak meninggalkan file sementara, lock, dan direktori setengah jadi. Episode ini membahas model sinyal BASH, memasang trap untuk EXIT dan SIGINT/SIGTERM, praktik mktemp + trap cleanup, lockfile anti-konkurensi, hingga jebakan trap yang tidak boleh terlewat.

Setelah di episode 18 sebelumnya kita membahas Error Handling & Robustness — exit codes, set -euo pipefail, dan kapan sengaja mengizinkan kegagalan — skrip kalian sekarang berani berhenti di tengah jalan ketika sesuatu salah. Itu kabar baik untuk kejujuran, tapi memunculkan pertanyaan baru yang tak kalah penting: ketika skrip berhenti mendadak, siapa yang membersihkan sisa-sisanya?
Bayangkan sebuah dapur profesional. Jurutama masak menyuruh asisten menyiapkan bahan: potong sayuran, rebus air, panaskan minyak. Lalu tiba-tiba alarm kebakaran berbunyi — semua orang keluar darurat. Ketika api padam dan tim kembali masuk, dapur itu penuh panci terbakar dan bahan berserakan. Jurutama masak yang baik tidak lari begitu saja; ia punya prosedur: matikan kompor, tutup gas, sebelum keluar. trap di BASH adalah prosedur darurat itu — kode yang dijamin dijalankan sebelum skrip pergi, apa pun penyebab kepergiannya.
Di dunia nyata, "panci terbakar" yang ditinggalkan skrip BASH adalah: file sementara di /tmp yang tidak pernah dihapus, lockfile yang membuat skrip lain terkunci selamanya, direktori yang baru dibuat setengah, atau terminal yang ditinggalkan dalam kondisi mode aneh. Sebuah skrip yang meninggalkan sampah seperti itu tidak selesai hanya karena prosesnya mati — ia mewariskan masalah kepada skrip berikutnya dan kepada manusia yang harus membersihkannya.
Di episode ini kita akan membedah model signal di Linux (SIGINT, SIGTERM, SIGHUP, dan lainnya), memasang trap untuk mengeksekusi handler saat sinyal datang atau saat skrip keluar, dan mempraktikkannya dalam tiga skenario paling penting di production: pembersihan file sementara dengan mktemp, lockfile untuk mencegah dua skrip berjalan bersamaan, dan pemulihan terminal saat skrip diinterupsi. Kita juga akan membahas jebakan-jebakan klasik — termasuk satu hal yang sering membuat pemula terjebak: trap EXIT berjalan bahkan saat skrip sukses.
Signal adalah pesan singkat yang dikirim sistem operasi ke sebuah proses untuk memberi tahu ada peristiwa yang perlu ditanggapi. Jangan bayangkan signal sebagai sesuatu yang rumit — bayangkan seperti bel di sekolah: ada bel masuk, bel istirahat, bel pulang. Masing-masing memberitahu murid (proses) apa yang seharusnya terjadi. Proses bisa mendengarkan, mengabaikan, atau — untuk beberapa sinyal — tidak bisa apa-apa.
Cara paling cepat melihat daftar sinyal di sistem kalian:
kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTPYang paling sering kalian jumpai di dunia scripting:
| Sinyal | Nomor | Sumber umum | Bisa di-trap? | Perilaku default |
|---|---|---|---|---|
| SIGHUP | 1 | Terminal ditutup / proses induk mati | Ya | Menghentikan proses |
| SIGINT | 2 | Ctrl+C di terminal | Ya | Menghentikan proses |
| SIGQUIT | 3 | Ctrl+\ | Ya | Berhenti + core dump |
| SIGTERM | 15 | kill <pid>, systemctl stop | Ya | Berhenti dengan "sopan" |
| SIGKILL | 9 | kill -9 <pid> | Tidak | Berhenti paksa |
| SIGSTOP | 19 | Ctrl+Z | Tidak | Menjeda proses |
| EXIT | — | Proses keluar dengan cara apa pun | Ya (spesial BASH) | — |
Kunci yang harus kalian hafal: SIGKILL (9) dan SIGSTOP (19) tidak bisa di-trap, di-ignore, atau ditangani. Itu adalah "kartu merah" terakhir dari kernel — tidak ada prosedur pembersihan yang akan berjalan. Inilah mengapa kill -9 selalu disebut sebagai opsi terakhir: ia tidak memberi proses kesempatan membersihkan apa pun. Ketika sistem membutuhkan shutdown yang rapi, ia mengirim SIGTERM lebih dulu dan memberi waktu; kill -9 adalah pemutus darurat.
trap: Memasang Jaring Pengamantrap adalah perintah yang memberitahu BASH: "ketika sinyal tertentu datang (atau ketika skrip keluar), jalankan perintah ini dulu." Sintaks dasarnya:
#!/bin/bash
cleanup() {
echo "Membersihkan..."
rm -rf "$TMP_DIR"
}
trap cleanup EXIT SIGINT SIGTERMDibaca: "saat skrip keluar (EXIT), di-Ctrl+C (SIGINT), atau diminta berhenti (SIGTERM), panggil fungsi cleanup." Ada dua bentuk penggunaan trap:
| Bentuk | Makna |
|---|---|
trap 'perintah' SIGNAMA | Pasang handler untuk sinyal |
trap - SIGNAMA | Hapus handler, kembalikan ke perilaku default |
trap 'perintah' EXIT | Jalankan sebelum skrip keluar (sukses maupun gagal) |
Perhatikan pertanyaan yang sering muncul: kenapa EXIT? EXIT bukan sinyal sungguhan dari kernel — ia adalah peristiwa "proses keluar" yang khusus BASH. Karena mencakup semua jalur keluar (normal, error, sinyal), trap ... EXIT adalah tempat paling andal untuk cleanup: satu baris, menjangkau segalanya.
Important
Poin yang paling sering mengejutkan: trap 'handler' EXIT berjalan meskipun skrip selesai dengan sukses. Jika kalian membuat trap 'echo BYE' EXIT, kalian akan selalu melihat BYE tercetak — baik skrip berakhir normal maupun karena error. Ini bukan bug; ini justru fitur inti. Cleanup tidak boleh membedakan "keluar karena sukses" dengan "keluar karena gagal" — keduanya sama-sama meninggalkan sampah. Jika kalian hanya ingin menangani kegagalan, gunakan trap pada sinyal spesifik (mis. trap cleanup SIGINT), bukan pada EXIT.
Ada nuansa penting kedua: timing evaluasi handler. Perhatikan perbedaan ini:
trap 'rm -f "$TMP_DIR"' EXIT — handler berisi tanda kutip tunggal, jadi $TMP_DIR dievaluasi saat trap dijalankan (saat keluar). Nilai terbaru variabel akan dipakai.trap "rm -f $TMP_DIR" EXIT — handler berisi tanda kutip ganda, jadi $TMP_DIR dievaluasi saat baris trap dieksekusi (saat dipasang). Nilai yang tersimpan adalah snapshot saat itu.Karena $TMP_DIR disetel setelah baris trap, bentuk kutip tunggal adalah yang benar. Inilah salah satu jebakan tersembunyi yang paling halus — dan masuk daftar pitfall di akhir episode.
mktemp + Trap CleanupKasus paling dasar dan paling sering: skrip yang membuat file sementara di /tmp, lalu harus memastikan file itu bersih walau skrip gagal di tengah jalan. Perintah mktemp membuat file/direktori sementara dengan nama acak yang unik — jauh lebih aman daripada menebak nama seperti /tmp/tmp1.log yang bisa bertabrakan:
#!/bin/bash
TMP_DIR="/tmp/mylog.$$"
mkdir -p "$TMP_DIR"
cp -r /var/log/nginx "$TMP_DIR"
tar czf /backup/logs.tar.gz -C "$TMP_DIR" .
echo "Selesai tanpa membersihkan!"Skrip di atas bekerja — dan setelah selesai, /tmp/mylog.<PID> tetap tertinggal selamanya. Sekarang versi yang benar:
#!/bin/bash
set -euo pipefail
cleanup() {
echo "Membersihkan file sementara..."
rm -rf "${TMP_DIR:-}"
}
TMP_DIR="$(mktemp -d)"
trap cleanup EXIT SIGINT SIGTERM
cp -r /var/log/nginx "$TMP_DIR"
tar czf /backup/logs.tar.gz -C "$TMP_DIR" .
echo "Backup selesai."Apa yang berubah:
mktemp -d menggantikan penebakan nama manual — nama direktori dijamin unik dan tersembunyi di lokasi aman.trap cleanup EXIT SIGINT SIGTERM dipasang segera setelah variabel TMP_DIR disetel. Posisinya penting: kalian tidak ingin trap dipasang sebelum ada apa pun yang perlu dibersihkan.${TMP_DIR:-} di dalam cleanup memakai nilai default kosong — sehingga jika TMP_DIR belum pernah disetel, rm -rf "" tidak akan terjadi (yang bisa berbahaya).Warning
Jangan pernah menulis rm -rf "${TMP_DIR}/" tanpa verifikasi bahwa TMP_DIR terisi — kesalahan kecil seperti variabel kosong bisa mengubah perintah menjadi rm -rf /. Pola rm -rf "${TMP_DIR:-}" plus guard [[ -n "${TMP_DIR:-}" ]] adalah standar industri untuk alasan ini. Satu karakter yang salah di cleanup lebih berbahaya daripada sampah yang ingin kalian buang.
Skenario klasik berikutnya: sebuah cron job dijadwalkan berjalan setiap jam, tapi satu kali eksekusi bisa memakan waktu lebih dari satu jam. Jika cron memicu eksekusi kedua sebelum yang pertama selesai, dua proses akan berebut resource yang sama — dua dump database, dua proses tar pada direktori yang sama. Hasilnya korupsi dan kekacauan.
Solusi standar: lockfile. Skrip membuat file penanda di awal dan menghapusnya di akhir. Jika lockfile sudah ada, skrip tahu ada proses lain sedang berjalan dan berhenti dengan sopan:
#!/bin/bash
set -euo pipefail
LOCKFILE="/var/lock/backup.job.lock"
cleanup() {
rmdir "$LOCKFILE" 2>/dev/null || true
echo "Lock dilepaskan."
}
if ! mkdir "$LOCKFILE" 2>/dev/null; then
echo "Job lain sedang berjalan. Keluar." >&2
exit 1
fi
trap cleanup EXIT SIGINT SIGTERM
# ... pekerjaan backup yang lama di sini ...
sleep 30
echo "Backup selesai."Mengapa mkdir dan bukan sekadar membuat file dengan touch? Karena mkdir bersifat atomik: hanya satu proses yang berhasil membuat direktori pada satu waktu. if ! mkdir ...; then berarti "jika direktori sudah ada (atau gagal dibuat), berhenti." Ini adalah primitif yang dijamin aman oleh kernel — tidak ada perlombaan antar proses.
Perhatikan juga rmdir (bukan rm -rf) di cleanup: rmdir hanya menghapus direktori kosong, jadi kalau ada yang tidak sengaja menaruh isi di dalam lock, cleanup menolak dengan aman (|| true membuat kegagalan itu tidak menghentikan skrip). Ini lapisan keamanan ekstra.
Tip
Untuk kebutuhan yang lebih serius — lock lintas proses dengan kadaluwarsa, atau ketika mkdir dirasa kurang ekspresif — kalian bisa memakai flock dari utilitas util-linux: flock /var/lock/backup.lock script_lain.sh mengunci file dan memblokir proses kedua sampai yang pertama selesai. flock bahkan lebih aman karena lock dibuka otomatis oleh kernel saat proses mati (termasuk kill -9), sesuatu yang tidak bisa dijamin oleh lockfile manual. Kenali keduanya: mkdir untuk skrip sederhana, flock untuk kebutuhan produksi yang ketat.
Ada satu kasus di mana cleanup bukan soal file, melainkan soal state terminal. Beberapa program (misalnya pembaca log interaktif atau aplikasi yang mematikan echo karakter) mengubah pengaturan terminal saat berjalan. Jika skrip semacam itu di-Ctrl+C di tengah jalan tanpa membersihkan, terminal bisa tertinggal dalam kondisi aneh — karakter tidak terlihat saat kalian mengetik, atau input tidak masuk.
trap bisa memulihkan pengaturan terminal sebelum keluar:
#!/bin/bash
set -euo pipefail
cleanup() {
stty echo # aktifkan kembali echo karakter
tput cnorm # kembalikan kursor yang terlihat
echo ""
echo "Terminal dipulihkan."
}
trap cleanup SIGINT SIGTERM EXIT
stty -echo # matikan echo saat skrip berjalan
printf "Ketik sesuatu (tidak terlihat): "
read -r input
trap - SIGINT # tugas selesai, lepaskan trap
echo ""
echo "Terima kasih, $input"Perhatikan baris trap - SIGINT: setelah bagian berbahaya selesai, kita melepas handler agar perilaku default (berhenti langsung saat Ctrl+C) kembali normal. Trap tidak harus hidup selamanya — lepaskan ketika tidak lagi dibutuhkan, persis seperti menutup katup gas setelah api padam.
1. trap EXIT berjalan walau sukses. Inilah jebakan yang paling sering membuat orang terkejut: handler di trap ... EXIT dieksekusi setiap kali skrip keluar — sukses, error, maupun karena sinyal. Jika kalian hanya ingin merespons kegagalan, jangan pakai EXIT; pakai sinyal spesifik. Jika kalian ingin cleanup, EXIT justru pilihan terbaik — karena sampah harus dibersihkan apa pun hasil akhirnya.
2. Timing evaluasi handler. Handler dalam kutip tunggal dievaluasi saat dijalankan (pakai nilai variabel terbaru); kutip ganda dievaluasi saat dipasang (snapshot lama). Gunakan kutip tunggal hampir selalu. Bug "trap tidak menghapus file yang baru dibuat" biasanya berakar dari sini.
3. Trap tidak menangani semua sinyal secara default. trap cleanup EXIT memang menjangkau banyak hal, tetapi tidak menangkap SIGKILL dan SIGSTOP (mustahil) serta tidak menangkap sinyal lain jika kalian tidak mendaftarkannya. Jika skrip bisa dihentikan dengan systemctl stop (yang mengirim SIGTERM) dan dengan Ctrl+C (SIGINT), daftarkan keduanya: trap cleanup EXIT SIGINT SIGTERM.
4. kill -9 tidak bisa di-trap. Tidak ada cara apa pun untuk membersihkan saat SIGKILL. Ini bukan kekurangan skrip kalian — ini desain kernel. Konsekuensinya: jangan bergantung pada trap untuk integritas data; gunakan lockfile flock (yang dilepas kernel) atau pastikan pemulihan bisa terjadi di eksekusi berikutnya.
5. Handler trap yang menulis output ke stdout. Jika skrip berjalan sebagai bagian dari pipeline (misalnya dicetak oleh cron), output dari handler trap yang memakai echo bisa merusak format output. Untuk handler yang informatif, tulis ke stderr (echo "pesan" >&2) atau ke file log — jangan ke stdout.
Caution
Jika handler trap memanggil perintah eksternal yang tidak ada (misalnya my-backup-tool yang belum terinstall), perintah itu gagal dan — dengan set -e — bisa menghentikan skrip dengan error yang membingungkan. Pastikan cleanup hanya memanggil perintah yang pasti tersedia (rm, rmdir, stty, tput), dan jika ragu, tambahkan || true di dalam handler. Handler adalah "pemadam kebakaran"; ia tidak boleh ikut terbakar.
Di episode 19 ini kalian telah memasang prosedur darurat di dapur skrip kalian. Kalian memahami model signal Linux (SIGINT untuk Ctrl+C, SIGTERM untuk kill/systemd, SIGKILL yang tak tertahankan), memasang trap untuk menjalankan handler saat EXIT maupun saat sinyal datang, dan mempraktikkannya dalam tiga skenario production: mktemp + trap cleanup untuk file sementara yang dijamin bersih, lockfile mkdir untuk mencegah dua job berjalan bersamaan, dan pemulihan terminal saat skrip diinterupsi.
Poin kunci yang harus kalian bawa pulang:
trap 'handler' EXIT SIGINT SIGTERM adalah jaring pengaman: satu baris, menjangkau semua jalur keluar.trap EXIT berjalan juga saat sukses — itu fitur untuk cleanup, bukan kejutan yang harus dilawan.kill -9 dan SIGKILL tidak bisa di-trap — rancang skrip yang bisa pulih tanpa trap.mkdir bersifat atomik dan aman untuk konkurensi sederhana; flock untuk kebutuhan yang lebih ketat.Sekarang skrip kalian tahu kapan harus berhenti dan cara meninggalkan ruangan dengan rapi. Namun ada satu keterampilan yang lebih berharga dari semua itu: kemampuan melihat apa yang sebenarnya dilakukan skrip ketika ia berjalan. Di episode 20 selanjutnya kita akan masuk ke Debugging Techniques: menelusuri eksekusi baris per baris dengan bash -x, memeriksa sintaks tanpa menjalankan dengan bash -n, memperkaya jejak dengan PS4, dan memasang breakpoint manual dengan trap DEBUG. Sampai jumpa di episode 20!