Tugas dari Bagian 1 sudah aman diulang; sekarang kita menyerahkannya kepada mesin pengulangan. Pada bagian ini tugas yang sama dipasang ke dua scheduler sekaligus: crontab user dan systemd timer. Tujuannya bukan memasang keduanya bersamaan di produksi, melainkan merasakan perbedaan nyata keduanya — dari environment eksekusi, cara mencatat log, sampai perilakunya terhadap jadwal yang terlewat — sehingga kita memilih berdasarkan pengalaman, bukan kebiasaan. Karena sesi terminal course ini tidak menjalankan daemon cron maupun systemd timer secara aktif, fokus kita adalah berkas jadwal yang benar dan tervalidasi: berkas crontab diperiksa validator sintaks, unit systemd diperiksa systemd-analyze verify, dan pemanggilan oleh scheduler disimulasikan runner.sh.
Berkas crontab: Lima Field dan Segala Jebakannya #
Cara pertama adalah crontab user. Alih-alih mengetik baris jadwal langsung ke editor lewat crontab -e, kita menulis berkas tugas.crontab — versi yang sama tersedia sebagai config/tugas.crontab pada bagian Berkas Sumber — lalu memasangnya sekaligus dengan crontab tugas.crontab. Menyimpan jadwal sebagai berkas memberi dua keuntungan: ia bisa disimpan bersama proyek, dan ia bisa diperiksa mesin sebelum dipasang.
Isinya lengkap dengan environment minimal yang dibutuhkan cron:
# tugas.crontab — jadwal tugas sinkronisasi (crontab user).
# Pasang dengan: crontab tugas.crontab
# Lihat dengan: crontab -l
# Sesuaikan /home/budi/otomasi dengan lokasi instalasi Anda.
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# menit jam tanggal bulan hari perintah
0 8 * * * flock -n /home/budi/otomasi/.tugas.lock /bin/bash /home/budi/otomasi/tugas.sh >> /home/budi/otomasi/log/tugas-output.log 2>&1Empat baris pertama yang bukan komentar adalah pengaturan environment, bukan jadwal: SHELL memastikan baris perintah dijalankan bash, PATH memperluas daftar program yang bisa ditemukan (cron bawaan hanya punya PATH minimal), dan MAILTO="" mematikan upaya mengirim email ke user lokal untuk output yang tidak tertangkap. Baris terakhir adalah jadwal + perintah. Lima field pertamanya menentukan waktunya:
| Field | Nilai kita | Rentang sah | Arti |
|---|---|---|---|
| menit | 0 | 0–59 | menit ke-0 |
| jam | 8 | 0–23 | pukul 08 |
| tanggal | * | 1–31 | setiap tanggal |
| bulan | * | 1–12 | setiap bulan |
| hari | * | 0–7 (0 dan 7 = Minggu) | setiap hari |
Gabungannya: setiap hari pukul 08:00. Sintaks lengkapnya — termasuk daftar 1,15, rentang 1-5, dan langkah */10 — dibahas pada Chapter Scheduling Scripts
dan didokumentasikan di crontab(5)
.
Sisa barisnya adalah perintah yang dijalankan cron, dan setiap bagiannya ada alasannya: flock -n menolak eksekusi bila tugas sebelumnya masih berjalan (keluar 1 tanpa menunggu), /bin/bash dan jalur skrip ditulis absolut karena PATH dan cwd milik cron tidak bisa diandalkan, sedangkan >> ... 2>&1 menangkap seluruh output normal maupun error ke satu berkas agar tidak hilang begitu saja.
Validator Sintaks Lima Field dengan awk #
Sintaks lima field mudah salah ketik — 8 yang menjadi 80, */2 yang kehilangan * — dan cron tidak akan mengeluh: baris yang salah cukup tidak pernah berjalan. Karena itu kita menulis validator awk yang memeriksa setiap baris jadwal: jumlah field, nilai di luar rentang, rentang terbalik, dan langkah yang tidak sah. Blok berikut menulis berkas tugas.crontab beserta validatornya, memeriksa jadwal kita, lalu mencoba validator pada berkas latihan yang sengaja salah:
cat > tugas.crontab <<'CRON'
# tugas.crontab — jadwal tugas sinkronisasi (crontab user).
# Pasang dengan: crontab tugas.crontab
# Lihat dengan: crontab -l
# Sesuaikan /home/budi/otomasi dengan lokasi instalasi Anda.
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# menit jam tanggal bulan hari perintah
0 8 * * * flock -n /home/budi/otomasi/.tugas.lock /bin/bash /home/budi/otomasi/tugas.sh >> /home/budi/otomasi/log/tugas-output.log 2>&1
CRON
cat > validasi-crontab.awk <<'AWK'
# validasi-crontab.awk — validator sintaks 5-field untuk berkas crontab.
# Pemakaian: awk -f validasi-crontab.awk berkas.crontab
# Keluaran: satu baris laporan per baris; exit 1 bila ada yang SALAH.
function cek_field(v, lo, hi, daftar, n, i, bagian, pos, langkah, rentang) {
n = split(v, daftar, ",")
for (i = 1; i <= n; i++) {
bagian = daftar[i]
pos = index(bagian, "/")
if (pos > 0) {
langkah = substr(bagian, pos + 1)
if (langkah !~ /^[0-9]+$/ || langkah + 0 < 1)
return 0
bagian = substr(bagian, 1, pos - 1)
}
if (bagian == "*")
continue
if (bagian ~ /^[0-9]+$/) {
if (bagian + 0 < lo || bagian + 0 > hi)
return 0
} else if (bagian ~ /^[0-9]+-[0-9]+$/) {
split(bagian, rentang, "-")
if (rentang[1] + 0 < lo || rentang[2] + 0 > hi)
return 0
if (rentang[1] + 0 > rentang[2] + 0)
return 0
} else {
return 0
}
}
return 1
}
/^[ \t]*#/ || /^[ \t]*$/ { next }
/^[A-Za-z_][A-Za-z0-9_]*=/ { printf "BARIS %d: pengaturan %s\n", NR, $1; next }
{
jumlah++
if (NF < 6) {
printf "BARIS %d SALAH: perlu 5 field jadwal + perintah\n", NR
salah++
next
}
if (!cek_field($1, 0, 59)) { printf "BARIS %d SALAH: menit %s\n", NR, $1; salah++; next }
if (!cek_field($2, 0, 23)) { printf "BARIS %d SALAH: jam %s\n", NR, $2; salah++; next }
if (!cek_field($3, 1, 31)) { printf "BARIS %d SALAH: tanggal %s\n", NR, $3; salah++; next }
if (!cek_field($4, 1, 12)) { printf "BARIS %d SALAH: bulan %s\n", NR, $4; salah++; next }
if (!cek_field($5, 0, 7)) { printf "BARIS %d SALAH: hari %s\n", NR, $5; salah++; next }
printf "BARIS %d OK: %s %s %s %s %s\n", NR, $1, $2, $3, $4, $5
}
END {
printf "%d baris jadwal diperiksa, %d SALAH\n", jumlah + 0, salah + 0
if (salah > 0)
exit 1
}
AWK
echo "== memeriksa tugas.crontab =="
awk -f validasi-crontab.awk tugas.crontab
echo "== mencoba validator pada jadwal yang salah =="
cat > jadwal-salah.crontab <<'CRON'
# jadwal-salah.crontab — latihan: temukan jadwal yang salah.
SHELL=/bin/bash
0 8 * * *
0 25 * * * /home/budi/otomasi/tugas.sh
61 * * * * /home/budi/otomasi/tugas.sh
0 8 5-2 * * /home/budi/otomasi/tugas.sh
CRON
awk -f validasi-crontab.awk jadwal-salah.crontab
echo "keluar=$?"Output yang Dihasilkan:
== memeriksa tugas.crontab ==
BARIS 6: pengaturan SHELL=/bin/bash
BARIS 7: pengaturan PATH=/usr/local/bin:/usr/bin:/bin
BARIS 8: pengaturan MAILTO=""
BARIS 11 OK: 0 8 * * *
1 baris jadwal diperiksa, 0 SALAH
== mencoba validator pada jadwal yang salah ==
BARIS 2: pengaturan SHELL=/bin/bash
BARIS 4 SALAH: perlu 5 field jadwal + perintah
BARIS 5 SALAH: jam 25
BARIS 6 SALAH: menit 61
BARIS 7 SALAH: tanggal 5-2
4 baris jadwal diperiksa, 4 SALAH
keluar=1Bedah validatornya:
function cek_field(v, lo, hi, ...): satu field boleh berisi daftar dipisah koma, dan setiap anggotanya boleh*, angka, rentanga-b, dengan atau tanpa/langkah. Parameter setelahhihanyalah variabel lokal — idiom awk untuk fungsi yang tidak menodai global.bagian + 0 < lo: nilai dibandingkan secara numerik dengan memaksa+ 0;25ditolak untuk jam karena melewati 23, dan rentang5-2ditolak karena terbalik.- Pola
/^[ \t]*#/ || /^[ \t]*$/: komentar dan baris kosong dilewati; pola/^[A-Za-z_][A-Za-z0-9_]*=/mengenali baris pengaturan sepertiPATH=...sehingga tidak dihitung sebagai jadwal. NF < 6: lima field jadwal plus satu perintah — baris0 8 * * *tanpa perintah adalah kesalahan yang paling sering lolos mata.exit 1di blokEND: validator gagal dengan kode keluar bila menemukan kesalahan, sehingga ia bisa dipakai sebagai gerbang pemasangan:awk -f validasi-crontab.awk tugas.crontab && crontab tugas.crontab.
Unit systemd: service, Timer, dan Verifikasinya #
Cara kedua adalah pasangan unit systemd — pekerjaan di tugas-sinkron.service, jadwal di tugas-sinkron.timer; nama pasangannya harus sama. Kedua berkas berikut identik dengan config/tugas-sinkron.service dan config/tugas-sinkron.timer, lalu diverifikasi dengan systemd-analyze verify — pemeriksaan sintaks yang berjalan tanpa perlu systemd aktif sebagai init:
cat > tugas-sinkron.service <<'UNIT'
# tugas-sinkron.service — unit pekerjaan untuk tugas sinkronisasi.
# Dipasangkan dengan tugas-sinkron.timer; letakkan keduanya di
# /etc/systemd/system/ lalu aktifkan TIMERNYA:
# systemctl daemon-reload
# systemctl enable --now tugas-sinkron.timer
# Sesuaikan /home/budi/otomasi dengan lokasi instalasi Anda.
[Unit]
Description=Tugas sinkronisasi folder sumber ke mirror
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /home/budi/otomasi/.tugas.lock /bin/bash /home/budi/otomasi/tugas.sh
UNIT
cat > tugas-sinkron.timer <<'UNIT'
# tugas-sinkron.timer — jadwal harian untuk tugas-sinkron.service.
# Persistent=true membuat timer mengejar jadwal yang terlewat saat
# mesin mati: begitu menyala, tugas langsung dijalankan satu kali.
[Unit]
Description=Jalankan tugas sinkronisasi setiap hari pukul 08:00
[Timer]
OnCalendar=08:00
Persistent=true
[Install]
WantedBy=timers.target
UNIT
systemd-analyze verify tugas-sinkron.service tugas-sinkron.timer && echo "kedua unit lolos verifikasi"
echo "== bentuk baku jadwal 08:00 menurut systemd =="
systemd-analyze calendar 08:00 | sed -n 's/^ *Normalized form: //p'Output yang Dihasilkan:
kedua unit lolos verifikasi
== bentuk baku jadwal 08:00 menurut systemd ==
*-*-* 08:00:00Bedah kedua unit:
Type=oneshot: pekerjaan berjalan sekali lalu selesai — bukan daemon yang hidup terus. Tugas harian kita memang begitu, dan timer adalah pemicunya.ExecStart=/usr/bin/flock -n ...: baris eksekusi memakai kunci dan berkas kunci yang sama dengan baris crontab (.tugas.lock). Satu kunci untuk dua scheduler: bila keduanya tidak sengaja terpasang bersamaan, yang terlambat menolak diri sendiri. Skrip dipanggil lewat/bin/bashabsolut sehingga permission eksekusi berkas tidak menjadi syarat.OnCalendar=08:00: bentuk singkat jadwal kalender. Perintahsystemd-analyze calendarmenunjukkan bentuk bakunya*-*-* 08:00:00— setiap hari, jam 08:00:00, dengan ketepatan hingga detik yang tidak dimiliki cron. Sintaks lengkap ada di systemd.time(7) .Persistent=true: jadwal yang terlewat karena mesin mati dikejar begitu mesin menyala kembali — tugas terlewat satu kali langsung berjalan, bukan menunggu jam 08:00 berikutnya.WantedBy=timers.target: hook agarsystemctl enabletahu target aktivasi; direktif timer lainnya dirinci di systemd.timer(5) .systemd-analyze verify ... && echo: verifikasi lolos tanpa keluhan — layaknyabash -nuntuk unit;ExecStartdiperiksa hingga keberadaan programnya,/usr/bin/flock.
cron atau systemd timer? #
Dua berkas jadwal di atas menjalankan tugas yang sama pada jam yang sama — perbedaannya ada di sekelilingnya:
| Aspek | crontab user | systemd timer |
|---|---|---|
| Environment eksekusi | minimal; PATH, SHELL, MAILTO diatur manual | terkelola layaknya service; bersih dan eksplisit |
| Log hasil tugas | redirect manual per baris (>> ... 2>&1) | otomatis terstruktur di journald |
| Jadwal terlewat saat mati | hilang begitu saja; perlu anacron | dikejar dengan Persistent=true |
| Eksekusi dobel | dicegah flock di baris jadwal | unit aktif tidak dinyalakan dua kali; flock tetap terpasang |
| Ketepatan waktu | menit | detik |
Aturan praktisnya sama seperti sebelumnya: tugas milik user yang sederhana dan portabel cocok untuk cron; tugas yang butuh log terstruktur, pengejaran jadwal, atau jangkar ke service lain lebih pas sebagai timer. Ringkasan yang lebih lengkap ada di wiki Arch tentang systemd timers .
Memasang dan Mensimulasikan #
Pemasangan sungguhan hanya dua perintah dan dijalankan di komputer Anda sendiri: crontab tugas.crontab untuk cron (lalu periksa dengan crontab -l), atau salin kedua unit ke /etc/systemd/system/ lalu sudo systemctl daemon-reload && sudo systemctl enable --now tugas-sinkron.timer untuk systemd. Setelah terpasang, systemctl list-timers merangkum jadwal berikutnya dan eksekusi terakhir; berikut contoh tampilan di sistem nyata:
$ systemctl list-timers --no-pager tugas-sinkron.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2026-01-16 08:00:00 WIB 17h Thu 2026-01-15 08:00:02 WIB 7h ago tugas-sinkron.timer tugas-sinkron.service
1 timers listed.Di lingkungan latihan kita memakai jalur yang bisa dijalankan kapan pun: runner.sh. Skrip ini meniru pemanggilan scheduler — environment dibersihkan dengan env -i seperti milik cron, eksekusi dibungkus flock -n seperti baris crontab, output ditangkap redirect — dengan satu penambahan: STAMP diteruskan sebagai “waktu eksekusi” sehingga jadwal bisa disimulasikan pada jam berapa pun dan hasilnya direproduksi. Versi lengkapnya tersedia sebagai src/runner.sh; blok berikut menjalankannya untuk dua hari berturut-turut, lalu mendemonstrasikan penolakan eksekusi ganda:
mkdir -p otomasi/sumber/laporan
printf 'daftar tugas minggu ini\n' > otomasi/sumber/tugas.txt
printf 'laporan penjualan kuartal pertama\n' > otomasi/sumber/laporan/penjualan.txt
cat > otomasi/tugas.sh <<'BASH'
#!/usr/bin/env bash
# tugas.sh — tugas terjadwal: sinkronisasi folder sumber ke mirror.
#
# Tugas ini dirancang untuk dipanggil scheduler (cron / systemd timer):
# - idempoten: berapa kali pun dijalankan, keadaan akhir mirror sama,
# - lokasi kerja dihitung dari letak skrip, bukan cwd pemanggil,
# sehingga aman dipanggil dari environment minimal milik cron,
# - setiap eksekusi menuliskan SATU baris log terstruktur yang bisa
# dibaca cek-jadwal.sh.
#
# Pemakaian:
# tugas.sh [--dry-run] [--verbose]
#
# Opsi:
# --dry-run tampilkan perubahan yang akan dilakukan tanpa menjalankannya
# --verbose tampilkan daftar perubahan per berkas
#
# Variabel lingkungan (berguna untuk pengujian deterministik):
# TUGAS_SRC folder sumber (default: <letak skrip>/sumber)
# TUGAS_DEST folder mirror (default: <letak skrip>/mirror)
# TUGAS_LOG berkas log (default: <letak skrip>/log/tugas.log)
# STAMP waktu eksekusi, format YYYYMMDDTHHMMSS (default: sekarang)
#
# Kode keluar:
# 0 sukses 1 opsi tidak dikenal
# 2 folder sumber tidak ada 3 rsync gagal
#
# Catatan: kunci anti-dobel (flock) sengaja TIDAK ada di skrip ini;
# kunci dipasang di lapisan scheduler (baris crontab / ExecStart unit)
# supaya satu berkas kunci melindungi kedua cara penjadwalan.
set -u
# Lokasi kerja dihitung dari letak skrip, bukan cwd pemanggil.
BASE=$(cd "$(dirname "$0")" && pwd)
TUGAS_SRC="${TUGAS_SRC:-$BASE/sumber}"
TUGAS_DEST="${TUGAS_DEST:-$BASE/mirror}"
TUGAS_LOG="${TUGAS_LOG:-$BASE/log/tugas.log}"
STAMP="${STAMP:-$(date +%Y%m%dT%H%M%S)}"
DRY_RUN=0
VERBOSE=0
for arg in "$@"; do
case $arg in
--dry-run) DRY_RUN=1 ;;
--verbose) VERBOSE=1 ;;
*)
printf 'tugas.sh: opsi tidak dikenal: %s\n' "$arg" >&2
printf 'pemakaian: tugas.sh [--dry-run] [--verbose]\n' >&2
exit 1
;;
esac
done
# waktu() : bentuk terbaca dari STAMP untuk baris-ringkasan di stdout.
waktu() {
printf '%s-%s-%s %s:%s:%s' \
"${STAMP:0:4}" "${STAMP:4:2}" "${STAMP:6:2}" \
"${STAMP:9:2}" "${STAMP:11:2}" "${STAMP:13:2}"
}
# Siapkan folder log sebelum bisa mencatat apa pun.
mkdir -p "$(dirname "$TUGAS_LOG")"
SECONDS=0
# --- 1. Validasi sumber --------------------------------------------------
if [ ! -d "$TUGAS_SRC" ]; then
printf '%s|GAGAL|keluar=2|durasi=%ds|sumber tidak ditemukan: %s\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" "$TUGAS_SRC" >> "$TUGAS_LOG"
printf 'tugas.sh: folder sumber tidak ditemukan: %s\n' "$TUGAS_SRC" >&2
exit 2
fi
# --- 2. Sinkronisasi rsync ------------------------------------------------
# --itemize-changes membuat rsync mencetak satu baris per perubahan
# (">f..." berkas dipindah, "*deleting" berkas dihapus); baris itulah
# yang dihitung menjadi ringkasan.
Opsi=(-a --delete --itemize-changes)
[ "$DRY_RUN" -eq 1 ] && Opsi+=(--dry-run)
if ! ubah=$(rsync "${Opsi[@]}" "$TUGAS_SRC/" "$TUGAS_DEST/"); then
printf '%s|GAGAL|keluar=3|durasi=%ds|rsync gagal\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" >> "$TUGAS_LOG"
printf 'tugas.sh: rsync gagal\n' >&2
exit 3
fi
total=$(find "$TUGAS_SRC" -type f | wc -l)
pindah=$(printf '%s\n' "$ubah" | grep -c '^[<>]f' || true)
hapus=$(printf '%s\n' "$ubah" | grep -c '^\*deleting' || true)
# --- 3. Dry-run: pratinjau tanpa mengubah apa pun, termasuk log -----------
if [ "$DRY_RUN" -eq 1 ]; then
printf '[%s] dry-run: %s -> %s\n' "$(waktu)" "$TUGAS_SRC" "$TUGAS_DEST"
[ -n "$ubah" ] && printf '%s\n' "$ubah"
printf '[%s] tidak ada yang diubah (dry-run)\n' "$(waktu)"
exit 0
fi
# --- 4. Catat log terstruktur + ringkasan ---------------------------------
printf '%s|OK|keluar=0|durasi=%ds|total=%d|pindah=%d|hapus=%d\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" "$total" "$pindah" "$hapus" >> "$TUGAS_LOG"
printf '[%s] mulai sinkronisasi: %s -> %s\n' "$(waktu)" "$TUGAS_SRC" "$TUGAS_DEST"
[ "$VERBOSE" -eq 1 ] && [ -n "$ubah" ] && printf '%s\n' "$ubah"
printf '[%s] %d berkas diperiksa, %d dipindah, %d dihapus\n' \
"$(waktu)" "$total" "$pindah" "$hapus"
printf '[%s] selesai: OK (%d detik)\n' "$(waktu)" "${TUGAS_DURASI:-$SECONDS}"
exit 0
BASH
cat > runner.sh <<'BASH'
#!/usr/bin/env bash
# runner.sh — tiruan scheduler (cron / systemd timer) untuk tugas.sh.
#
# Lingkungan latihan tidak menjalankan crond maupun systemd timer, jadi
# runner.sh memainkan peran scheduler: memanggil tugas persis seperti
# baris crontab / ExecStart unit melakukannya — environment minimal,
# dilindungi flock -n, stdout+stderr ditangkap redirect — lalu
# melaporkan kode keluarnya. STAMP menjadi "waktu eksekusi" sehingga
# jadwal bisa direproduksi kapan pun tanpa menunggu jam 08:00.
#
# Pemakaian:
# STAMP=20260115T080000 bash runner.sh [folder-tugas]
#
# folder-tugas : folder berisi tugas.sh (default: folder saat ini)
#
# Kode keluar:
# 0 tugas sukses 1 kunci flock sibuk (tugas ditolak)
# 2/3 kode keluar tugas.sh lainnya
set -u
DIR="${1:-.}"
STAMP="${STAMP:-$(date +%Y%m%dT%H%M%S)}"
mkdir -p "$DIR/log"
# env -i membersihkan lingkungan seperti milik cron: hanya PATH minimal
# dan HOME yang diteruskan, ditambah STAMP untuk determinisme.
env -i PATH=/usr/local/bin:/usr/bin:/bin HOME="$DIR" STAMP="$STAMP" \
flock -n "$DIR/.tugas.lock" /bin/bash "$DIR/tugas.sh" \
>> "$DIR/log/tugas-output.log" 2>&1
rc=$?
printf 'scheduler: tugas keluar=%d\n' "$rc"
exit "$rc"
BASH
echo "== simulasi jadwal: dua hari berturut-turut =="
STAMP=20260115T080000 bash runner.sh otomasi
STAMP=20260116T080000 bash runner.sh otomasi
echo "== output yang ditangkap redirect scheduler =="
cat otomasi/log/tugas-output.log
echo "== log terstruktur =="
cat otomasi/log/tugas.log
echo "== tugas masih berjalan: jadwal berikutnya ditolak flock =="
flock otomasi/.tugas.lock sleep 5 &
sleep 1
STAMP=20260116T080100 bash runner.sh otomasi
echo "jumlah baris log (tidak bertambah): $(wc -l < otomasi/log/tugas.log)"
waitOutput yang Dihasilkan:
== simulasi jadwal: dua hari berturut-turut ==
scheduler: tugas keluar=0
scheduler: tugas keluar=0
== output yang ditangkap redirect scheduler ==
[2026-01-15 08:00:00] mulai sinkronisasi: /home/budi/otomasi/sumber -> /home/budi/otomasi/mirror
[2026-01-15 08:00:00] 2 berkas diperiksa, 2 dipindah, 0 dihapus
[2026-01-15 08:00:00] selesai: OK (0 detik)
[2026-01-16 08:00:00] mulai sinkronisasi: /home/budi/otomasi/sumber -> /home/budi/otomasi/mirror
[2026-01-16 08:00:00] 2 berkas diperiksa, 0 dipindah, 0 dihapus
[2026-01-16 08:00:00] selesai: OK (0 detik)
== log terstruktur ==
20260115T080000|OK|keluar=0|durasi=0s|total=2|pindah=2|hapus=0
20260116T080000|OK|keluar=0|durasi=0s|total=2|pindah=0|hapus=0
== tugas masih berjalan: jadwal berikutnya ditolak flock ==
scheduler: tugas keluar=1
jumlah baris log (tidak bertambah): 2Tiga bagian akhir blok itu adalah perilaku scheduler yang sesungguhnya. Kedua pemanggilan pertama keluar 0 — tugas sehat di dua hari berbeda, dan pindah=2 lalu pindah=0 di log membuktikan idempotensi tetap terjaga lewat lapisan scheduler. Bagian terakhir mensimulasikan tugas lambat: kunci dipegang sleep 5 seolah tugas kemarin masih berjalan, pemanggilan berikutnya ditolak dengan keluar 1 — persis keluaran flock -n saat kunci sibuk, didokumentasikan di flock(1)
— dan jumlah baris log tetap dua karena tugas yang ditolak memang tidak pernah dijalankan.
Dua scheduler kini terpasang penuh di atas tugas yang sama, masing-masing dengan jadwal yang tervalidasi dan kunci yang berbagi. Yang belum kita miliki adalah cara membaca hasilnya tanpa membuka log mentah satu per satu — itu pekerjaan Bagian 3, tempat log terstruktur berubah menjadi diagnosis.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
src/cek-jadwal.sh
Unduh berkas#!/usr/bin/env bash
# cek-jadwal.sh — periksa kesehatan tugas terjadwal dari berkas lognya.
#
# Membaca log terstruktur tugas.sh dan menjawab tiga pertanyaan admin:
# kapan eksekusi terakhir, berapa jam sejak sukses terakhir, dan sudah
# berapa kali gagal berturut-turut. Cocok dijalankan manual saat curiga
# jadwal macet, atau dijadwalkan sendiri sebagai tugas kedua.
#
# Pemakaian:
# cek-jadwal.sh <berkas-log> [batas-jam]
#
# batas-jam : berapa jam tanpa sukses sebelum dianggap TERLAMPAT
# (default 25: jadwal harian + toleransi 1 jam)
# NOW : variabel lingkungan "waktu sekarang", format
# YYYYMMDDTHHMMSS (default: waktu nyata). Dipakai agar
# pengujian umur log deterministik.
#
# Kode keluar:
# 0 SEHAT 1 TERLAMPAT / belum ada eksekusi
# 2 GAGAL berturut-turut 3 berkas log tidak ditemukan
set -u
LOG="${1:-}"
BATAS="${2:-25}"
NOW="${NOW:-$(date +%Y%m%dT%H%M%S)}"
if [ -z "$LOG" ]; then
printf 'pemakaian: cek-jadwal.sh <berkas-log> [batas-jam]\n' >&2
exit 3
fi
if [ ! -f "$LOG" ]; then
printf 'cek-jadwal.sh: berkas log tidak ditemukan: %s\n' "$LOG" >&2
exit 3
fi
# ke_epoch() : ubah STAMP (YYYYMMDDTHHMMSS) menjadi detik epoch (UTC).
ke_epoch() {
date -u -d "${1:0:4}-${1:4:2}-${1:6:2} ${1:9:2}:${1:11:2}:${1:13:2}" +%s
}
# tampil() : bentuk terbaca dari STAMP untuk laporan.
tampil() {
printf '%s-%s-%s %s:%s:%s' \
"${1:0:4}" "${1:4:2}" "${1:6:2}" "${1:9:2}" "${1:11:2}" "${1:13:2}"
}
# --- 1. Baca seluruh baris log -------------------------------------------
n=0
terakhir_status=""
terakhir_stamp=""
terakhir_keluar=""
sukses_stamp=""
berturut=0
while IFS='|' read -r stamp status sisa; do
# Hanya baris berformat STAMP|STATUS|... yang diperhitungkan.
case $stamp in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T*) ;;
*) continue ;;
esac
n=$((n + 1))
terakhir_stamp=$stamp
terakhir_status=$status
keluar=${sisa%%|*}
terakhir_keluar=${keluar#keluar=}
if [ "$status" = "OK" ]; then
sukses_stamp=$stamp
berturut=0
elif [ "$status" = "GAGAL" ]; then
berturut=$((berturut + 1))
fi
done < "$LOG"
# --- 2. Laporan -----------------------------------------------------------
printf 'log : %s\n' "$LOG"
printf 'eksekusi : %d\n' "$n"
if [ "$n" -eq 0 ]; then
printf 'status : BELUM — belum ada eksekusi tercatat\n'
exit 1
fi
if [ "$terakhir_status" = "GAGAL" ]; then
printf 'terakhir : GAGAL pada %s (keluar=%s)\n' \
"$(tampil "$terakhir_stamp")" "$terakhir_keluar"
else
printf 'terakhir : OK pada %s\n' "$(tampil "$terakhir_stamp")"
fi
if [ -n "$sukses_stamp" ]; then
sukses_epoch=$(ke_epoch "$sukses_stamp")
umur=$(( ($(ke_epoch "$NOW") - sukses_epoch) / 3600 ))
printf 'sukses : %s (%d jam lalu)\n' "$(tampil "$sukses_stamp")" "$umur"
else
umur=999999
printf 'sukses : belum pernah\n'
fi
printf 'berturut : %d gagal beruntun\n' "$berturut"
# --- 3. Klasifikasi -------------------------------------------------------
# Gagal berturut-turut paling parah: tugas jalan tapi selalu gagal.
# Umur melebihi batas: tugas tidak jalan (atau jalan tanpa hasil).
if [ "$berturut" -ge 2 ]; then
printf 'status : GAGAL — %d kali gagal berturut-turut\n' "$berturut"
exit 2
fi
if [ -z "$sukses_stamp" ]; then
printf 'status : TERLAMPAT — belum pernah sukses (batas %d jam)\n' "$BATAS"
exit 1
fi
if [ "$umur" -gt "$BATAS" ]; then
printf 'status : TERLAMBAT — %d jam tanpa sukses (batas %d jam)\n' \
"$umur" "$BATAS"
exit 1
fi
printf 'status : SEHAT — sukses terakhir %d jam lalu\n' "$umur"
exit 0
src/runner.sh
Unduh berkas#!/usr/bin/env bash
# runner.sh — tiruan scheduler (cron / systemd timer) untuk tugas.sh.
#
# Lingkungan latihan tidak menjalankan crond maupun systemd timer, jadi
# runner.sh memainkan peran scheduler: memanggil tugas persis seperti
# baris crontab / ExecStart unit melakukannya — environment minimal,
# dilindungi flock -n, stdout+stderr ditangkap redirect — lalu
# melaporkan kode keluarnya. STAMP menjadi "waktu eksekusi" sehingga
# jadwal bisa direproduksi kapan pun tanpa menunggu jam 08:00.
#
# Pemakaian:
# STAMP=20260115T080000 bash runner.sh [folder-tugas]
#
# folder-tugas : folder berisi tugas.sh (default: folder saat ini)
#
# Kode keluar:
# 0 tugas sukses 1 kunci flock sibuk (tugas ditolak)
# 2/3 kode keluar tugas.sh lainnya
set -u
DIR="${1:-.}"
STAMP="${STAMP:-$(date +%Y%m%dT%H%M%S)}"
mkdir -p "$DIR/log"
# env -i membersihkan lingkungan seperti milik cron: hanya PATH minimal
# dan HOME yang diteruskan, ditambah STAMP untuk determinisme.
env -i PATH=/usr/local/bin:/usr/bin:/bin HOME="$DIR" STAMP="$STAMP" \
flock -n "$DIR/.tugas.lock" /bin/bash "$DIR/tugas.sh" \
>> "$DIR/log/tugas-output.log" 2>&1
rc=$?
printf 'scheduler: tugas keluar=%d\n' "$rc"
exit "$rc"
src/tugas.sh
Unduh berkas#!/usr/bin/env bash
# tugas.sh — tugas terjadwal: sinkronisasi folder sumber ke mirror.
#
# Tugas ini dirancang untuk dipanggil scheduler (cron / systemd timer):
# - idempoten: berapa kali pun dijalankan, keadaan akhir mirror sama,
# - lokasi kerja dihitung dari letak skrip, bukan cwd pemanggil,
# sehingga aman dipanggil dari environment minimal milik cron,
# - setiap eksekusi menuliskan SATU baris log terstruktur yang bisa
# dibaca cek-jadwal.sh.
#
# Pemakaian:
# tugas.sh [--dry-run] [--verbose]
#
# Opsi:
# --dry-run tampilkan perubahan yang akan dilakukan tanpa menjalankannya
# --verbose tampilkan daftar perubahan per berkas
#
# Variabel lingkungan (berguna untuk pengujian deterministik):
# TUGAS_SRC folder sumber (default: <letak skrip>/sumber)
# TUGAS_DEST folder mirror (default: <letak skrip>/mirror)
# TUGAS_LOG berkas log (default: <letak skrip>/log/tugas.log)
# STAMP waktu eksekusi, format YYYYMMDDTHHMMSS (default: sekarang)
#
# Kode keluar:
# 0 sukses 1 opsi tidak dikenal
# 2 folder sumber tidak ada 3 rsync gagal
#
# Catatan: kunci anti-dobel (flock) sengaja TIDAK ada di skrip ini;
# kunci dipasang di lapisan scheduler (baris crontab / ExecStart unit)
# supaya satu berkas kunci melindungi kedua cara penjadwalan.
set -u
# Lokasi kerja dihitung dari letak skrip, bukan cwd pemanggil.
BASE=$(cd "$(dirname "$0")" && pwd)
TUGAS_SRC="${TUGAS_SRC:-$BASE/sumber}"
TUGAS_DEST="${TUGAS_DEST:-$BASE/mirror}"
TUGAS_LOG="${TUGAS_LOG:-$BASE/log/tugas.log}"
STAMP="${STAMP:-$(date +%Y%m%dT%H%M%S)}"
DRY_RUN=0
VERBOSE=0
for arg in "$@"; do
case $arg in
--dry-run) DRY_RUN=1 ;;
--verbose) VERBOSE=1 ;;
*)
printf 'tugas.sh: opsi tidak dikenal: %s\n' "$arg" >&2
printf 'pemakaian: tugas.sh [--dry-run] [--verbose]\n' >&2
exit 1
;;
esac
done
# waktu() : bentuk terbaca dari STAMP untuk baris-ringkasan di stdout.
waktu() {
printf '%s-%s-%s %s:%s:%s' \
"${STAMP:0:4}" "${STAMP:4:2}" "${STAMP:6:2}" \
"${STAMP:9:2}" "${STAMP:11:2}" "${STAMP:13:2}"
}
# Siapkan folder log sebelum bisa mencatat apa pun.
mkdir -p "$(dirname "$TUGAS_LOG")"
SECONDS=0
# --- 1. Validasi sumber --------------------------------------------------
if [ ! -d "$TUGAS_SRC" ]; then
printf '%s|GAGAL|keluar=2|durasi=%ds|sumber tidak ditemukan: %s\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" "$TUGAS_SRC" >> "$TUGAS_LOG"
printf 'tugas.sh: folder sumber tidak ditemukan: %s\n' "$TUGAS_SRC" >&2
exit 2
fi
# --- 2. Sinkronisasi rsync ------------------------------------------------
# --itemize-changes membuat rsync mencetak satu baris per perubahan
# (">f..." berkas dipindah, "*deleting" berkas dihapus); baris itulah
# yang dihitung menjadi ringkasan.
Opsi=(-a --delete --itemize-changes)
[ "$DRY_RUN" -eq 1 ] && Opsi+=(--dry-run)
if ! ubah=$(rsync "${Opsi[@]}" "$TUGAS_SRC/" "$TUGAS_DEST/"); then
printf '%s|GAGAL|keluar=3|durasi=%ds|rsync gagal\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" >> "$TUGAS_LOG"
printf 'tugas.sh: rsync gagal\n' >&2
exit 3
fi
total=$(find "$TUGAS_SRC" -type f | wc -l)
pindah=$(printf '%s\n' "$ubah" | grep -c '^[<>]f' || true)
hapus=$(printf '%s\n' "$ubah" | grep -c '^\*deleting' || true)
# --- 3. Dry-run: pratinjau tanpa mengubah apa pun, termasuk log -----------
if [ "$DRY_RUN" -eq 1 ]; then
printf '[%s] dry-run: %s -> %s\n' "$(waktu)" "$TUGAS_SRC" "$TUGAS_DEST"
[ -n "$ubah" ] && printf '%s\n' "$ubah"
printf '[%s] tidak ada yang diubah (dry-run)\n' "$(waktu)"
exit 0
fi
# --- 4. Catat log terstruktur + ringkasan ---------------------------------
printf '%s|OK|keluar=0|durasi=%ds|total=%d|pindah=%d|hapus=%d\n' \
"$STAMP" "${TUGAS_DURASI:-$SECONDS}" "$total" "$pindah" "$hapus" >> "$TUGAS_LOG"
printf '[%s] mulai sinkronisasi: %s -> %s\n' "$(waktu)" "$TUGAS_SRC" "$TUGAS_DEST"
[ "$VERBOSE" -eq 1 ] && [ -n "$ubah" ] && printf '%s\n' "$ubah"
printf '[%s] %d berkas diperiksa, %d dipindah, %d dihapus\n' \
"$(waktu)" "$total" "$pindah" "$hapus"
printf '[%s] selesai: OK (%d detik)\n' "$(waktu)" "${TUGAS_DURASI:-$SECONDS}"
exit 0
tests/run-tests.sh
Unduh berkas#!/usr/bin/env bash
# run-tests.sh — test suite untuk paket otomasi-dengan-cron-dan-systemd.
#
# Suite menyiapkan folder kerja terisolasi (mktemp) beserta data
# contohnya, menyalin skrip dari src/ dan berkas dari config/, lalu
# menjalankan rangkaian pengujian dengan assertion sederhana:
# - idempotensi tugas.sh (dua eksekusi, hasil akhir identik),
# - --dry-run tidak mengubah mirror maupun log,
# - baris crontab lolos validator sintaks 5-field dan benar-benar
# bisa dijalankan (jalur disesuaikan ke folder kerja),
# - unit systemd lolos systemd-analyze verify,
# - flock menolak eksekusi ganda lewat baris crontab dan runner.sh,
# - cek-jadwal.sh mengklasifikasi tiga kondisi log dengan benar.
#
# Jalankan dari mana saja: bash tests/run-tests.sh
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")
PASS=0
FAIL=0
# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
if [ "$1" = "$2" ]; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
fi
}
# --- siapkan folder kerja terisolasi ---------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/src/tugas.sh" "$PROJ_DIR/src/cek-jadwal.sh" \
"$PROJ_DIR/src/runner.sh" "$WORK/"
cd "$WORK" || exit 1
# Data contoh (fixture) untuk tugas sinkronisasi.
mkdir -p sumber/laporan
printf 'daftar tugas minggu ini\n' > sumber/tugas.txt
printf 'laporan penjualan kuartal pertama\n' > sumber/laporan/penjualan.txt
# --- uji 1: eksekusi pertama sukses ---------------------------------------
STAMP=20260115T080000 bash tugas.sh > /dev/null 2>&1
assert_eq 0 "$?" "eksekusi pertama keluar 0"
diff -r sumber mirror > /dev/null 2>&1
assert_eq 0 "$?" "mirror identik dengan sumber"
# --- uji 2: idempotensi — eksekusi ulang tidak mengubah hasil -------------
cp -a mirror potret-1
STAMP=20260115T080500 bash tugas.sh > /dev/null 2>&1
assert_eq 0 "$?" "eksekusi kedua keluar 0"
diff -r potret-1 mirror > /dev/null 2>&1
assert_eq 0 "$?" "eksekusi ulang tidak mengubah mirror"
pindah2=$(sed -n '2p' log/tugas.log | grep -c 'pindah=0')
assert_eq 1 "$pindah2" "eksekusi kedua tidak memindah berkas (idempoten)"
# --- uji 3: dry-run tidak mengubah mirror maupun log -----------------------
printf 'catatan baru\n' > sumber/catatan-baru.txt
n_log=$(wc -l < log/tugas.log)
STAMP=20260115T081000 bash tugas.sh --dry-run > /dev/null 2>&1
assert_eq 0 "$?" "dry-run keluar 0"
[ ! -f mirror/catatan-baru.txt ]
assert_eq 0 "$?" "dry-run tidak menyalin berkas baru"
n_log2=$(wc -l < log/tugas.log)
assert_eq "$n_log" "$n_log2" "dry-run tidak menulis log"
# --- uji 4: eksekusi berikutnya membawa berkas baru ------------------------
STAMP=20260115T081500 bash tugas.sh > /dev/null 2>&1
assert_eq 0 "$?" "eksekusi ketiga keluar 0"
diff -r sumber mirror > /dev/null 2>&1
assert_eq 0 "$?" "mirror mengikuti sumber terbaru"
# --- uji 5: opsi salah dan sumber hilang ----------------------------------
bash tugas.sh --salah-opsi > /dev/null 2>&1
assert_eq 1 "$?" "opsi tidak dikenal keluar 1"
TUGAS_SRC="$WORK/tidak-ada" STAMP=20260116T080000 bash tugas.sh > /dev/null 2>&1
assert_eq 2 "$?" "sumber hilang keluar 2"
gagal=$(tail -n 1 log/tugas.log | grep -c 'GAGAL|keluar=2')
assert_eq 1 "$gagal" "kegagalan sumber tercatat di log"
# --- uji 6: crontab lolos validator sintaks 5-field ------------------------
cat > validasi-crontab.awk <<'AWK'
# validasi-crontab.awk — validator sintaks 5-field untuk berkas crontab.
function cek_field(v, lo, hi, daftar, n, i, bagian, pos, langkah, rentang) {
n = split(v, daftar, ",")
for (i = 1; i <= n; i++) {
bagian = daftar[i]
pos = index(bagian, "/")
if (pos > 0) {
langkah = substr(bagian, pos + 1)
if (langkah !~ /^[0-9]+$/ || langkah + 0 < 1)
return 0
bagian = substr(bagian, 1, pos - 1)
}
if (bagian == "*")
continue
if (bagian ~ /^[0-9]+$/) {
if (bagian + 0 < lo || bagian + 0 > hi)
return 0
} else if (bagian ~ /^[0-9]+-[0-9]+$/) {
split(bagian, rentang, "-")
if (rentang[1] + 0 < lo || rentang[2] + 0 > hi)
return 0
if (rentang[1] + 0 > rentang[2] + 0)
return 0
} else {
return 0
}
}
return 1
}
/^[ \t]*#/ || /^[ \t]*$/ { next }
/^[A-Za-z_][A-Za-z0-9_]*=/ { printf "BARIS %d: pengaturan %s\n", NR, $1; next }
{
jumlah++
if (NF < 6) {
printf "BARIS %d SALAH: perlu 5 field jadwal + perintah\n", NR
salah++
next
}
if (!cek_field($1, 0, 59)) { printf "BARIS %d SALAH: menit %s\n", NR, $1; salah++; next }
if (!cek_field($2, 0, 23)) { printf "BARIS %d SALAH: jam %s\n", NR, $2; salah++; next }
if (!cek_field($3, 1, 31)) { printf "BARIS %d SALAH: tanggal %s\n", NR, $3; salah++; next }
if (!cek_field($4, 1, 12)) { printf "BARIS %d SALAH: bulan %s\n", NR, $4; salah++; next }
if (!cek_field($5, 0, 7)) { printf "BARIS %d SALAH: hari %s\n", NR, $5; salah++; next }
printf "BARIS %d OK: %s %s %s %s %s\n", NR, $1, $2, $3, $4, $5
}
END {
printf "%d baris jadwal diperiksa, %d SALAH\n", jumlah + 0, salah + 0
if (salah > 0)
exit 1
}
AWK
awk -f validasi-crontab.awk "$PROJ_DIR/config/tugas.crontab" > hasil-vaildasi.txt
assert_eq 0 "$?" "config/tugas.crontab lolos validator 5-field"
ringkasan=$(tail -n 1 hasil-vaildasi.txt)
assert_eq "1 baris jadwal diperiksa, 0 SALAH" "$ringkasan" "validator menemukan 1 jadwal valid"
cat > salah-jadwal.crontab <<'CRON'
# salah-jadwal.crontab — fixture berisi jadwal yang salah.
SHELL=/bin/bash
0 8 * * *
0 25 * * * /home/budi/otomasi/tugas.sh
61 * * * * /home/budi/otomasi/tugas.sh
CRON
awk -f validasi-crontab.awk salah-jadwal.crontab > hasil-salah.txt
assert_eq 1 "$?" "validator menolak jadwal yang salah (keluar 1)"
n_salah=$(grep -c 'SALAH:' hasil-salah.txt)
assert_eq 3 "$n_salah" "tiga baris salah terdeteksi"
# --- uji 7: baris crontab benar-benar bisa dijalankan ----------------------
# Ambil baris jadwal pertama, ganti jalur instalasi ke folder kerja,
# lalu jalankan persis seperti yang akan dilakukan cron.
baris=$(awk '!/^[ \t]*#/ && !/^[ \t]*$/ && !/^[A-Za-z_][A-Za-z0-9_]*=/ { print; exit }' \
"$PROJ_DIR/config/tugas.crontab")
read -r _m _h _dm _mo _dw cmd <<< "$baris"
cmd=${cmd//\/home\/budi\/otomasi/$WORK}
mkdir -p log
STAMP=20260117T080000 bash -c "$cmd"
assert_eq 0 "$?" "baris crontab (jalur disesuaikan) sukses dijalankan"
n_ok=$(grep -c '^20260117T080000|OK' log/tugas.log)
assert_eq 1 "$n_ok" "eksekusi via baris crontab tercatat di log"
[ -s log/tugas-output.log ]
assert_eq 0 "$?" "output tugas tertangkap redirect crontab"
# --- uji 8: flock menolak eksekusi ganda ------------------------------------
flock "$WORK/.tugas.lock" sleep 1 &
sleep 0.3
n_sebelum=$(wc -l < log/tugas.log)
STAMP=20260117T080500 bash -c "$cmd" > /dev/null 2>&1
assert_eq 1 "$?" "baris crontab ditolak flock saat kunci sibuk"
n_sesudah=$(wc -l < log/tugas.log)
assert_eq "$n_sebelum" "$n_sesudah" "eksekusi yang ditolak tidak menulis log"
wait
# --- uji 9: unit systemd lolos verify ----------------------------------------
systemd-analyze verify "$PROJ_DIR/config/tugas-sinkron.service" \
"$PROJ_DIR/config/tugas-sinkron.timer" > /dev/null 2>&1
assert_eq 0 "$?" "unit service+timer lolos systemd-analyze verify"
# --- uji 10: runner menjalankan tugas seperti scheduler ----------------------
STAMP=20260118T080000 bash runner.sh "$WORK" > /dev/null 2>&1
assert_eq 0 "$?" "runner menjalankan tugas seperti scheduler"
n_ok=$(grep -c '^20260118T080000|OK' log/tugas.log)
assert_eq 1 "$n_ok" "eksekusi via runner tercatat di log"
# --- uji 11: runner ditolak saat kunci sibuk (dobel) ------------------------
flock "$WORK/.tugas.lock" sleep 1 &
sleep 0.3
n_sebelum=$(wc -l < log/tugas.log)
STAMP=20260118T080500 bash runner.sh "$WORK" > /dev/null 2>&1
assert_eq 1 "$?" "runner ditolak flock saat tugas lain berjalan"
n_sesudah=$(wc -l < log/tugas.log)
assert_eq "$n_sebelum" "$n_sesudah" "runner yang ditolak tidak menulis log"
wait
# --- uji 12: cek-jadwal mengklasifikasi tiga kondisi log --------------------
cat > log-sehat.log <<'FIX'
20260113T080000|OK|keluar=0|durasi=0s|total=3|pindah=0|hapus=0
20260114T080000|OK|keluar=0|durasi=0s|total=3|pindah=1|hapus=0
20260115T080000|OK|keluar=0|durasi=0s|total=3|pindah=0|hapus=0
20260116T080000|OK|keluar=0|durasi=0s|total=3|pindah=2|hapus=0
FIX
cat > log-terlambat.log <<'FIX'
20260112T080000|OK|keluar=0|durasi=0s|total=3|pindah=0|hapus=0
20260113T080000|OK|keluar=0|durasi=0s|total=3|pindah=2|hapus=1
FIX
cat > log-gagal.log <<'FIX'
20260114T080000|OK|keluar=0|durasi=0s|total=3|pindah=0|hapus=0
20260115T080000|GAGAL|keluar=3|durasi=0s|rsync gagal
20260116T080000|GAGAL|keluar=3|durasi=0s|rsync gagal
FIX
NOW=20260116T120000 bash cek-jadwal.sh log-sehat.log > laporan-sehat.txt
assert_eq 0 "$?" "log sehat diklasifikasi SEHAT (keluar 0)"
n=$(grep -c 'sukses terakhir 4 jam lalu' laporan-sehat.txt)
assert_eq 1 "$n" "umur sukses dihitung: 4 jam"
NOW=20260116T120000 bash cek-jadwal.sh log-terlambat.log > laporan-terlambat.txt
assert_eq 1 "$?" "log berhenti diklasifikasi TERLAMBAT (keluar 1)"
n=$(grep -c '76 jam tanpa sukses' laporan-terlambat.txt)
assert_eq 1 "$n" "umur tanpa sukses dihitung: 76 jam"
NOW=20260116T120000 bash cek-jadwal.sh log-gagal.log > laporan-gagal.txt
assert_eq 2 "$?" "log gagal berturut diklasifikasi GAGAL (keluar 2)"
n=$(grep -c '2 kali gagal berturut-turut' laporan-gagal.txt)
assert_eq 1 "$n" "gagal berturut terdeteksi: 2 kali"
bash cek-jadwal.sh log-tidak-ada.log > /dev/null 2>&1
assert_eq 3 "$?" "berkas log yang hilang dilaporkan (keluar 3)"
# --- ringkasan -----------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]