Cara cepat dan kotor untuk memastikan hanya satu contoh skrip shell yang berjalan pada satu waktu


Jawaban:


109

Berikut ini adalah implementasi yang menggunakan lockfile dan gema PID ke dalamnya. Ini berfungsi sebagai perlindungan jika prosesnya dibunuh sebelum mengeluarkan pidfile :

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

Kuncinya di sini adalah kill -0yang tidak memberikan sinyal apa pun tetapi hanya memeriksa apakah ada proses dengan PID yang diberikan. Juga panggilan ke trapakan memastikan bahwa lockfile dihapus bahkan ketika proses Anda terbunuh (kecuali kill -9).


73
Seperti yang telah disebutkan dalam komentar pada jawaban antera, ini memiliki kesalahan fatal - jika skrip lain dimulai antara cek dan gema, Anda bersulang.
Paul Tomblin

1
Trik symlink rapi, tetapi jika pemilik lockfile adalah kill -9'd atau sistem crash, masih ada kondisi perlombaan untuk membaca symlink, perhatikan pemilik hilang, dan kemudian hapus. Saya bertahan dengan solusi saya.
bmdhacks

9
Pemeriksaan dan pembuatan atom tersedia di shell menggunakan flock (1) atau lockfile (1). Lihat jawaban lain.
dmckee --- ex-moderator kitten

3
Lihat balasan saya untuk cara portabel melakukan pemeriksaan atom dan membuat tanpa harus bergantung pada utilitas seperti kawanan atau file kunci.
lhunath

2
Ini bukan atom dan karena itu tidak berguna. Anda memerlukan mekanisme atom untuk pengujian & set.
K Richard Pixley

214

Menggunakan flock(1) untuk membuat kunci scoped eksklusif pada deskriptor file. Dengan cara ini Anda bahkan dapat menyinkronkan berbagai bagian skrip.

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

Ini memastikan bahwa kode antara (dan )dijalankan hanya oleh satu proses pada satu waktu dan bahwa proses tidak menunggu terlalu lama untuk kunci.

Peringatan: perintah khusus ini adalah bagian dari util-linux. Jika Anda menjalankan sistem operasi selain Linux, mungkin atau mungkin tidak tersedia.


11
Apa 200 itu? Dikatakan "fd" di manul, tapi aku tidak tahu apa artinya itu.
chovy

4
@chovy "file descriptor", bilangan bulat yang menunjuk file yang terbuka.
Alex B

6
Jika ada orang lain yang bertanya-tanya: Sintaks ( command A ) command Bmemanggil subshell untuk command A. Didokumentasikan di tldp.org/LDP/abs/html/subshells.html . Saya masih tidak yakin tentang waktu doa subkulit dan perintah B.
Dr. Jan-Philip Gehrcke

1
Saya pikir kode di dalam sub-shell harus lebih seperti: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fisehingga jika batas waktu terjadi (beberapa proses lain file terkunci), skrip ini tidak melanjutkan dan memodifikasi file. Mungkin ... argumen tandingannya adalah 'tetapi jika perlu 10 detik dan kunci masih tidak tersedia, itu tidak akan pernah tersedia', mungkin karena proses memegang kunci tidak berakhir (mungkin sedang dijalankan di bawah debugger?).
Jonathan Leffler

1
File yang dialihkan ke hanya folder tempat untuk kunci untuk bertindak, tidak ada data berarti masuk ke dalamnya. The exitadalah dari bagian dalam ( ). Ketika subproses berakhir, kunci dilepaskan secara otomatis, karena tidak ada proses menahannya.
clacke

158

Semua pendekatan yang menguji keberadaan "file kunci" cacat.

Mengapa? Karena tidak ada cara untuk memeriksa apakah suatu file ada dan membuatnya dalam satu aksi atom. Karena ini; ada kondisi perlombaan yang AKAN membuat upaya Anda untuk saling menyingkirkan pengecualian.

Sebagai gantinya, Anda perlu menggunakan mkdir. mkdirmembuat direktori jika belum ada, dan jika itu ada, ia menetapkan kode keluar. Lebih penting lagi, ia melakukan semua ini dalam aksi atom tunggal sehingga sempurna untuk skenario ini.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

Untuk semua detail, lihat BashFAQ yang luar biasa: http://mywiki.wooledge.org/BashFAQ/045

Jika Anda ingin merawat kunci basi, fuser (1) berguna. Satu-satunya downside di sini adalah bahwa operasi memakan waktu sekitar satu detik, jadi itu tidak instan.

Inilah fungsi yang saya tulis sekali yang menyelesaikan masalah menggunakan fuser:

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

Anda dapat menggunakannya dalam skrip seperti:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

Jika Anda tidak peduli tentang portabilitas (solusi ini harus bekerja pada hampir semua kotak UNIX), Linux fuser (1) menawarkan beberapa opsi tambahan dan ada juga kawanan (1) .


1
Anda dapat menggabungkan if ! mkdirbagian dengan memeriksa apakah proses dengan PID disimpan (pada startup yang sukses) di dalam lockdir benar-benar berjalan dan identik dengan skrip untuk perlindungan stalen. Ini juga akan melindungi terhadap penggunaan kembali PID setelah reboot, dan bahkan tidak memerlukan fuser.
Tobias Kienzler

4
Memang benar bahwa mkdirtidak didefinisikan sebagai operasi atom dan karena itu "efek samping" adalah detail implementasi dari sistem file. Saya sepenuhnya percaya padanya jika dia mengatakan NFS tidak mengimplementasikannya dengan cara atom. Meskipun saya tidak curiga Anda /tmpakan menjadi bagian NFS dan kemungkinan akan disediakan oleh fs yang mengimplementasikan mkdirsecara atom.
lhunath

5
Tetapi ada cara untuk memeriksa keberadaan file biasa dan membuatnya secara atomis jika tidak: gunakan lnuntuk membuat tautan keras dari file lain. Jika Anda memiliki sistem file aneh yang tidak menjamin itu, Anda dapat memeriksa inode file baru setelahnya untuk melihat apakah itu sama dengan file asli.
Juan Cespedes

4
Ada adalah 'cara untuk memeriksa apakah file yang ada dan menciptakan dalam tindakan atom tunggal' - itu open(... O_CREAT|O_EXCL). Anda hanya perlu program pengguna yang sesuai untuk melakukannya, seperti lockfile-create(dalam lockfile-progs) atau dotlockfile(dalam liblockfile-bin). Dan pastikan Anda membersihkan dengan benar (misalnya trap EXIT), atau menguji untuk kunci basi (misalnya dengan --use-pid).
Toby Speight

5
"Semua pendekatan yang menguji keberadaan" file kunci "cacat. Mengapa? Karena tidak ada cara untuk memeriksa apakah file ada dan membuatnya dalam aksi atom tunggal." - Untuk membuatnya menjadi atom, itu harus dilakukan di tingkat kernel - dan hal ini dilakukan pada tingkat kernel dengan kawanan (1) linux.die.net/man/1/flock yang muncul dari tanggal hak cipta manusia sudah ada sejak setidaknya 2006. Jadi saya membuat downvote (- 1), bukan masalah pribadi, cukup yakin bahwa menggunakan alat yang diimplementasikan oleh kernel yang disediakan oleh pengembang kernel adalah benar.
Craig Hicks

42

Ada pembungkus di sekitar kawanan (2) panggilan sistem yang disebut, tidak terbayangkan, kawanan (1). Ini membuatnya relatif mudah untuk mendapatkan kunci eksklusif tanpa khawatir tentang pembersihan dll. Ada contoh di halaman manual tentang bagaimana menggunakannya dalam skrip shell.


3
The flock()system call tidak POSIX dan tidak bekerja untuk file di NFS.
maxschlepzig

17
Menjalankan dari pekerjaan Cron yang saya gunakan flock -x -n %lock file% -c "%command%"untuk memastikan hanya satu instance yang pernah dijalankan.
Ryall

Ah, bukannya kawanan yang tidak imajinatif (1) mereka seharusnya pergi dengan sesuatu seperti kawanan (U). ... .itu memiliki keakraban untuk itu. . Sepertinya saya pernah mendengar itu sebelumnya.
Kent Kruckeberg

Perlu dicatat bahwa dokumentasi flock (2) hanya menentukan penggunaan dengan file, tetapi dokumentasi flock (1) menentukan penggunaan dengan file atau direktori. Dokumentasi kawanan (1) tidak eksplisit tentang bagaimana menunjukkan perbedaan selama pembuatan, tetapi saya menganggap itu dilakukan dengan menambahkan "/" final. Bagaimanapun, jika kawanan (1) dapat menangani direktori tetapi kawanan (2) tidak bisa, maka kawanan (1) tidak diterapkan hanya pada kawanan (2).
Craig Hicks

27

Anda memerlukan operasi atom, seperti kawanan domba, kalau tidak ini akan gagal.

Tetapi apa yang harus dilakukan jika kawanan tidak tersedia. Yah ada mkdir. Itu operasi atom juga. Hanya satu proses yang akan menghasilkan mkdir yang berhasil, semua yang lain akan gagal.

Jadi kodenya adalah:

if mkdir /var/lock/.myscript.exclusivelock
then
  # do stuff
  :
  rmdir /var/lock/.myscript.exclusivelock
fi

Anda perlu menjaga kunci basi jika tidak crash skrip Anda tidak akan pernah berjalan lagi.


1
Jalankan ini beberapa kali secara bersamaan (seperti "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") dan skrip akan bocor beberapa kali.
Nippysaurus

7
@Nippysaurus: Metode penguncian ini tidak bocor. Apa yang Anda lihat adalah skrip awal berakhir sebelum semua salinan diluncurkan, sehingga yang lain bisa (dengan benar) mendapatkan kuncinya. Untuk menghindari false positive ini, tambahkan sleep 10sebelum rmdirdan coba cascade lagi - tidak ada yang akan "bocor".
Sir Athos

Sumber lain mengklaim mkdir tidak bersifat atom pada beberapa sistem file seperti NFS. Dan btw saya telah melihat kesempatan di mana pada NFS mkdir rekursif bersamaan menyebabkan kesalahan kadang-kadang dengan pekerjaan matriks jenkins. Jadi saya cukup yakin itu masalahnya. Tapi mkdir cukup bagus untuk kasus penggunaan IMO yang tidak terlalu menuntut.
akostadinov

Anda dapat menggunakan opsi noclobber Bash'es dengan file biasa.
Palec

26

Untuk membuat penguncian dapat diandalkan, Anda memerlukan operasi atom. Banyak dari proposal di atas tidak bersifat atom. Utilitas lockfile (1) yang diusulkan terlihat menjanjikan seperti halaman manual yang disebutkan, bahwa "NFS-resistant". Jika OS Anda tidak mendukung lockfile (1) dan solusi Anda harus bekerja pada NFS, Anda tidak memiliki banyak pilihan ....

NFSv2 memiliki dua operasi atom:

  • symlink
  • ganti nama

Dengan NFSv3, panggilan buat juga atom.

Operasi direktori BUKAN atom di bawah NFSv2 dan NFSv3 (lihat buku 'NFS Illustrated' oleh Brent Callaghan, ISBN 0-201-32570-5; Brent adalah veteran NFS di Sun).

Mengetahui hal ini, Anda dapat menerapkan spin-lock untuk file dan direktori (di shell, bukan PHP):

kunci dir saat ini:

while ! ln -s . lock; do :; done

mengunci file:

while ! ln -s ${f} ${f}.lock; do :; done

membuka kunci dir saat ini (asumsi, proses yang berjalan benar-benar mendapatkan kunci):

mv lock deleteme && rm deleteme

membuka kunci file (asumsi, proses yang berjalan benar-benar mendapatkan kunci):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

Buang juga bukan atom, oleh karena itu pertama ganti nama (yang merupakan atom) dan kemudian hapus.

Untuk symlink dan rename panggilan, kedua nama file harus berada pada sistem file yang sama. Proposal saya: hanya menggunakan nama file sederhana (tidak ada jalur) dan meletakkan file dan mengunci ke direktori yang sama.


Halaman NFS Illustrated mana yang mendukung pernyataan bahwa mkdir tidak atomik atas NFS?
maxschlepzig

Terima kasih untuk teknik ini. Implementasi shell mutex tersedia di lib shell baru saya: github.com/Offirmo/offirmo-shell-lib , lihat "mutex". Ini digunakan lockfilejika tersedia, atau mundur ke symlinkmetode ini jika tidak.
Offirmo

Bagus. Sayangnya metode ini tidak menyediakan cara untuk menghapus kunci basi secara otomatis.
Richard Hansen

Untuk membuka dua tahap ( mv, rm), harus rm -fdigunakan, daripada rmdalam kasus dua proses P1, P2 balap? Sebagai contoh, P1 mulai membuka dengan mv, kemudian P2 mengunci, lalu P2 membuka kunci (keduanya mvdan rm), akhirnya P1 mencoba rmdan gagal.
Matt Wallis

1
@MattWallis Itu masalah terakhir bisa dengan mudah diatasi dengan termasuk $$dalam ${f}.deletemenama file.
Stefan Majewsky

23

Opsi lain adalah menggunakan noclobberopsi shell dengan menjalankan set -C. Kemudian> akan gagal jika file tersebut sudah ada.

Secara singkat:

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

Ini menyebabkan shell memanggil:

open(pathname, O_CREAT|O_EXCL)

yang secara atom menciptakan file atau gagal jika file tersebut sudah ada.


Menurut komentar di BashFAQ 045 , ini mungkin gagal ksh88, tetapi berfungsi di semua shell saya:

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

Menarik yang pdkshmenambahkan O_TRUNCbendera, tetapi jelas berlebihan:
Anda membuat file kosong, atau tidak melakukan apa-apa.


Cara Anda melakukannya rmtergantung pada bagaimana Anda ingin pintu keluar yang tidak bersih ditangani.

Hapus saat keluar bersih

Menjalankan baru gagal sampai masalah yang menyebabkan keluarnya yang tidak bersih untuk diselesaikan dan lockfile dihapus secara manual.

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

Hapus kapan saja

Menjalankan baru berhasil asalkan skrip belum berjalan.

trap 'rm "$lockfile"' EXIT

Pendekatan yang sangat baru ... ini tampaknya menjadi salah satu cara untuk mencapai atomicity menggunakan file kunci daripada direktori kunci.
Matt Caldwell

Pendekatan yang bagus. :-) Pada perangkap EXIT, harus membatasi proses mana yang dapat membersihkan file kunci. Sebagai contoh: trap 'if [[$ (cat "$ lockfile") == "$$"]]; lalu rm "$ lockfile"; fi 'EXIT
Kevin Seifert

1
File kunci tidak lebih dari NFS. itu sebabnya orang pindah ke menggunakan direktori kunci.
K Richard Pixley

20

Anda dapat menggunakan GNU Parallelini karena berfungsi sebagai mutex ketika dipanggil sebagai sem. Jadi, secara konkret, Anda dapat menggunakan:

sem --id SCRIPTSINGLETON yourScript

Jika Anda ingin waktu habis juga, gunakan:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Batas waktu <0 berarti keluar tanpa menjalankan skrip jika semaphore tidak dirilis dalam batas waktu, batas waktu> 0 berarti tetap menjalankan skrip.

Perhatikan bahwa Anda harus memberi nama (dengan --id) jika tidak default ke terminal pengendali.

GNU Parallel adalah instalasi yang sangat sederhana di kebanyakan platform Linux / OSX / Unix - ini hanya skrip Perl.


Sayang sekali orang enggan untuk mengecilkan jawaban yang tidak berguna: ini mengarah pada jawaban relevan baru yang terkubur di tumpukan sampah.
Dmitry Grigoryev

4
Kami hanya perlu banyak upvotes. Ini jawaban yang rapi dan sedikit diketahui. (Meskipun untuk menjadi OP OPIC ingin cepat-dan-kotor sedangkan ini cepat-dan-bersih!) Lebih lanjut tentang semdi pertanyaan terkait unix.stackexchange.com/a/322200/199525 .
Sebagian Mendung

16

Untuk skrip shell, saya cenderung menggunakan yang mkdirlebih flockkarena membuat kunci lebih portabel.

Either way, menggunakan set -etidak cukup. Itu hanya keluar dari skrip jika ada perintah yang gagal. Kunci Anda masih akan tertinggal.

Untuk pembersihan kunci yang benar, Anda benar-benar harus mengatur jebakan Anda ke sesuatu seperti kode psuedo ini (diangkat, disederhanakan, dan belum teruji tetapi dari skrip yang digunakan secara aktif):

#=======================================================================
# Predefined Global Variables
#=======================================================================

TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] \
    && mkdir -p $TMP_DIR \
    && chmod 700 $TMPDIR

LOCK_DIR=$TMP_DIR/lock

#=======================================================================
# Functions
#=======================================================================

function mklock {
    __lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID

    # If it can create $LOCK_DIR then no other instance is running
    if $(mkdir $LOCK_DIR)
    then
        mkdir $__lockdir  # create this instance's specific lock in queue
        LOCK_EXISTS=true  # Global
    else
        echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
        exit 1001  # Or work out some sleep_while_execution_lock elsewhere
    fi
}

function rmlock {
    [[ ! -d $__lockdir ]] \
        && echo "WARNING: Lock is missing. $__lockdir does not exist" \
        || rmdir $__lockdir
}

#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or 
#         there will be *NO CLEAN UP*. You'll have to manually remove 
#         any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {

    # Place your clean up logic here 

    # Remove the LOCK
    [[ -n $LOCK_EXISTS ]] && rmlock
}

function __sig_int {
    echo "WARNING: SIGINT caught"    
    exit 1002
}

function __sig_quit {
    echo "SIGQUIT caught"
    exit 1003
}

function __sig_term {
    echo "WARNING: SIGTERM caught"    
    exit 1015
}

#=======================================================================
# Main
#=======================================================================

# Set TRAPs
trap __sig_exit EXIT    # SIGEXIT
trap __sig_int INT      # SIGINT
trap __sig_quit QUIT    # SIGQUIT
trap __sig_term TERM    # SIGTERM

mklock

# CODE

exit # No need for cleanup code here being in the __sig_exit trap function

Inilah yang akan terjadi. Semua jebakan akan menghasilkan jalan keluar sehingga fungsi tersebut __sig_exitakan selalu terjadi (kecuali SIGKILL) yang membersihkan kunci Anda.

Catatan: nilai keluar saya bukan nilai rendah. Mengapa? Berbagai sistem pemrosesan batch membuat atau memiliki harapan angka 0 hingga 31. Mengaturnya ke hal lain, saya dapat membuat skrip dan aliran batch saya bereaksi sesuai dengan pekerjaan atau skrip batch sebelumnya.


2
Script Anda terlalu bertele-tele, bisa jadi saya pikir jauh lebih pendek, tetapi secara keseluruhan, ya, Anda harus mengatur jebakan untuk melakukan ini dengan benar. Saya juga akan menambahkan SIGHUP.
mojuba

Ini berfungsi dengan baik, kecuali tampaknya memeriksa $ LOCK_DIR sedangkan itu menghapus $ __ lockdir. Mungkin saya harus menyarankan saat melepas kunci yang akan Anda lakukan rm -r $ LOCK_DIR?
bevada

Terima kasih atas sarannya. Kode di atas diangkat dan ditempatkan dalam mode kode psuedo sehingga perlu penyetelan berdasarkan penggunaan orang. Namun, saya sengaja pergi dengan rmdir dalam kasus saya karena rmdir dengan aman menghapus direktori only_if mereka kosong. Jika orang-orang menempatkan sumber daya di dalamnya seperti file PID, dll. Mereka harus mengubah pembersihan kunci mereka menjadi lebih agresif rm -r $LOCK_DIRatau bahkan memaksanya seperlunya (seperti yang telah saya lakukan juga dalam kasus-kasus khusus seperti memegang file awal relatif). Bersulang.
Mark Stinson

Sudahkah Anda menguji exit 1002?
Gilles Quenot

13

Sangat cepat dan sangat kotor? Satu baris ini di bagian atas skrip Anda akan berfungsi:

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

Tentu saja, pastikan nama skrip Anda unik. :)


Bagaimana saya mensimulasikan ini untuk mengujinya? Apakah ada cara untuk memulai skrip dua kali dalam satu baris dan mungkin mendapat peringatan, jika sudah berjalan?
rubo77

2
Ini tidak berfungsi sama sekali! Mengapa memeriksa -gt 2? grep tidak selalu menemukan dirinya di hasil ps!
rubo77

pgreptidak dalam POSIX. Jika Anda ingin ini berfungsi dengan baik, Anda perlu POSIX psdan memproses hasilnya.
Palec

Pada OSX -ctidak ada, Anda harus menggunakan | wc -l. Tentang perbandingan angka: -gt 1diperiksa sejak instance pertama melihat dirinya sendiri.
Benjamin Peter

6

Berikut adalah pendekatan yang menggabungkan penguncian direktori atom dengan tanda centang untuk kunci basi melalui PID dan mulai ulang jika basi. Juga, ini tidak bergantung pada bashisme apa pun.

#!/bin/dash

SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"

if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi

trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT


echo hello

sleep 30s

echo bye

5

Buat file kunci di lokasi yang diketahui dan periksa keberadaannya saat skrip dimulai? Menempatkan PID dalam file mungkin membantu jika seseorang berusaha melacak instance yang salah yang mencegah eksekusi skrip.


5

Contoh ini dijelaskan dalam man flock, tetapi perlu beberapa perbaikan, karena kita harus mengelola bug dan kode keluar:

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.

( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands

) 200>/var/lock/.myscript.exclusivelock   #exit subprocess

FLOCKEXIT=$?  #save exitcode status
    #do some finish commands

exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

Anda dapat menggunakan metode lain, daftar proses yang saya gunakan di masa lalu. Tetapi ini lebih rumit dari metode di atas. Anda harus mendaftar proses dengan ps, filter dengan namanya, filter grep tambahan -v grep untuk menghapus parasit dan akhirnya hitung dengan grep -c. dan bandingkan dengan angka. Rumit dan tidak pasti


1
Anda dapat menggunakan ln -s, karena ini dapat membuat symlink hanya ketika tidak ada file atau symlink, sama seperti mkdir. banyak proses sistem yang menggunakan symlink di masa lalu, misalnya init atau inetd. synlink membuat proses id, tetapi benar-benar menunjuk ke tidak ada. selama bertahun-tahun perilaku ini berubah. proses menggunakan kawanan dan semaphore.
Znik

5

Jawaban yang ada diposting baik mengandalkan utilitas CLI flockatau tidak mengunci file kunci dengan benar. Utilitas kawanan tidak tersedia di semua sistem non-Linux (yaitu FreeBSD), dan tidak berfungsi dengan baik pada NFS.

Pada hari-hari awal saya administrasi sistem dan pengembangan sistem, saya diberitahu bahwa metode yang aman dan relatif portabel untuk membuat file kunci adalah membuat file temp menggunakan mkemp(3)ataumkemp(1) , menulis informasi pengidentifikasi ke file temp (yaitu PID), kemudian hard link file temp ke file kunci. Jika tautan berhasil, maka Anda telah berhasil mendapatkan kunci.

Saat menggunakan kunci dalam skrip shell, saya biasanya menempatkan suatu obtain_lock()fungsi di profil bersama dan kemudian mengambilnya dari skrip. Di bawah ini adalah contoh dari fungsi kunci saya:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"

  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"

  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"

  return 0;
};

Berikut ini adalah contoh cara menggunakan fungsi kunci:

#!/bin/sh

. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"

clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}

obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM

# bulk of script

clean_up
exit 0
# end of script

Ingatlah untuk menelepon clean_updi titik keluar apa pun dalam skrip Anda.

Saya telah menggunakan hal di atas di lingkungan Linux dan FreeBSD.


4

Saat menargetkan mesin Debian saya menemukan lockfile-progspaket menjadi solusi yang baik. procmailjuga dilengkapi dengan lockfilealat. Namun kadang-kadang saya terjebak dengan keduanya.

Inilah solusi saya yang menggunakan mkdiruntuk atom-ness dan file PID untuk mendeteksi kunci basi. Kode ini saat ini sedang diproduksi pada pengaturan Cygwin dan berfungsi dengan baik.

Untuk menggunakannya cukup menelepon exclusive_lock_requireketika Anda perlu mendapatkan akses eksklusif ke sesuatu. Parameter nama kunci opsional memungkinkan Anda berbagi kunci antara skrip yang berbeda. Ada juga dua fungsi level yang lebih rendah ( exclusive_lock_trydan exclusive_lock_retry) jika Anda membutuhkan sesuatu yang lebih kompleks.

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}

Terima kasih, coba sendiri di cygwin dan lulus tes sederhana.
ndemou

4

Jika batasan kawanan, yang telah dijelaskan di tempat lain di utas ini, bukan masalah bagi Anda, maka ini harusnya berfungsi:

#!/bin/bash

{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit

    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

3
Hanya berpikir saya akan menunjukkan bahwa -x (tulis kunci) sudah diatur secara default.
Keldon Alleyne

dan -nakan exit 1segera jika tidak bisa mendapatkan kunci
Anentropic

Terima kasih @KeldonAlleyne, saya memperbarui kode untuk menghapus "-x" karena ini adalah default.
presto8

3

Beberapa unix memiliki lockfileyang sangat mirip dengan yang telah disebutkan flock.

Dari halaman manual:

lockfile dapat digunakan untuk membuat satu atau beberapa file semaphore. Jika file kunci tidak dapat membuat semua file yang ditentukan (dalam urutan yang ditentukan), itu menunggu sleeptime (default ke 8) detik dan mencoba lagi file terakhir yang tidak berhasil. Anda dapat menentukan jumlah percobaan yang harus dilakukan sampai kegagalan dikembalikan. Jika jumlah coba lagi adalah -1 (default, yaitu -r-1) lockfile akan coba lagi selamanya.


bagaimana cara mendapatkan lockfileutilitasnya ??
Offirmo

lockfiledidistribusikan bersama procmail. Juga ada alternatif dotlockfileyang sesuai dengan liblockfilepaket. Mereka berdua mengklaim dapat bekerja dengan andal pada NFS.
Tn. Deathless

3

Sebenarnya walaupun jawaban bmdhacks hampir bagus, ada sedikit kemungkinan skrip kedua dijalankan setelah pertama kali memeriksa lockfile dan sebelum ia menulisnya. Jadi mereka berdua akan menulis file kunci dan mereka berdua akan berjalan. Berikut ini cara membuatnya pasti:

lockfile=/var/lock/myscript.lock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
  # or you can decide to skip the "else" part if you want
  echo "Another instance is already running!"
fi

Itu noclobber pilihan akan memastikan bahwa perintah redirect akan gagal jika file sudah ada. Jadi perintah pengalihan sebenarnya atom - Anda menulis dan memeriksa file dengan satu perintah. Anda tidak perlu menghapus file kunci di akhir file - itu akan dihapus oleh perangkap. Saya harap ini membantu orang-orang yang akan membacanya nanti.

PS Saya tidak melihat bahwa Mikel sudah menjawab pertanyaan dengan benar, meskipun ia tidak memasukkan perintah trap untuk mengurangi kemungkinan file kunci akan ditinggalkan setelah menghentikan skrip dengan Ctrl-C misalnya. Jadi ini solusi lengkapnya


3

Saya ingin menghapus lockfile, lockdirs, program penguncian khusus dan bahkan pidofkarena tidak ditemukan di semua instalasi Linux. Juga ingin memiliki kode sesederhana mungkin (atau setidaknya beberapa baris mungkin). ifPernyataan paling sederhana , dalam satu baris:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

1
Ini peka terhadap keluaran 'ps', di komputer saya (Ubuntu 14.04, / bin / ps dari procps-ng versi 3.3.9) perintah 'ps axf' mencetak karakter pohon ascii yang mengganggu angka bidang. Ini bekerja untuk saya: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill

2

Saya menggunakan pendekatan sederhana yang menangani file kunci basi.

Perhatikan bahwa beberapa solusi di atas yang menyimpan pid, abaikan fakta bahwa pid dapat membungkus. Jadi - hanya memeriksa apakah ada proses yang valid dengan pid yang disimpan tidak cukup, terutama untuk skrip yang berjalan lama.

Saya menggunakan noclobber untuk memastikan hanya satu skrip yang dapat membuka dan menulis ke file kunci pada satu waktu. Selanjutnya, saya menyimpan informasi yang cukup untuk mengidentifikasi secara unik suatu proses di lockfile. Saya mendefinisikan set data untuk secara unik mengidentifikasi suatu proses menjadi pid, ppid, lstart.

Ketika skrip baru dimulai, jika gagal membuat file kunci, itu kemudian memverifikasi bahwa proses yang membuat file kunci masih ada. Jika tidak, kami menganggap proses asli meninggal karena kematian yang tidak dapat dilacak, dan meninggalkan file kunci basi. Script baru kemudian mengambil kepemilikan file kunci, dan semuanya baik-baik saja di dunia.

Harus bekerja dengan banyak shell di berbagai platform. Cepat, portabel, dan sederhana.

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

Saya memberi Anda +1 untuk solusi yang berbeda. Meskipun itu tidak berfungsi baik dalam AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: daftar tidak valid dengan -o.) Tidak HP-UX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: opsi ilegal - o). Terima kasih.
Tagar

2

Tambahkan baris ini di awal skrip Anda

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

Ini kode boilerplate dari kawanan manusia.

Jika Anda ingin lebih banyak login, gunakan yang ini

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

Ini mengatur dan memeriksa kunci menggunakan flock utilitas. Kode ini mendeteksi jika dijalankan pertama kali dengan memeriksa variabel FLOCKER, jika tidak disetel ke nama skrip, maka ia mencoba memulai skrip lagi menggunakan kawanan secara acak dan dengan variabel FLOCKER diinisialisasi, jika FLOCKER diatur dengan benar, kemudian kawanan pada iterasi sebelumnya berhasil dan tidak apa-apa untuk melanjutkan. Jika kunci sibuk, gagal dengan kode keluar yang dapat dikonfigurasi.

Tampaknya tidak berfungsi pada Debian 7, tetapi tampaknya bekerja kembali dengan paket util-linux 2.25 eksperimental. Itu menulis "kawanan: ... File teks sibuk". Ini bisa diganti dengan menonaktifkan izin menulis pada skrip Anda.


1

PID dan file kunci jelas yang paling dapat diandalkan. Ketika Anda mencoba menjalankan program, ia dapat memeriksa file kunci mana dan jika ada, itu dapat digunakan psuntuk melihat apakah prosesnya masih berjalan. Jika tidak, skrip dapat mulai, memperbarui PID di lockfile sendiri.


1

Saya menemukan bahwa solusi bmdhack adalah yang paling praktis, setidaknya untuk kasus penggunaan saya. Menggunakan flock dan lockfile mengandalkan penghapusan lockfile menggunakan rm ketika skrip berakhir, yang tidak selalu dapat dijamin (misalnya, kill -9).

Saya akan mengubah satu hal kecil tentang solusi bmdhack: Ini membuat titik menghapus file kunci, tanpa menyatakan bahwa ini tidak perlu untuk kerja semaphore yang aman ini. Penggunaannya untuk kill -0 memastikan bahwa file lockfile lama untuk proses mati hanya akan diabaikan / ditulis berlebihan.

Oleh karena itu solusi saya yang disederhanakan adalah dengan menambahkan berikut ini di bagian atas singleton Anda:

## Test the lock
LOCKFILE=/tmp/singleton.lock 
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "Script already running. bye!"
    exit 
fi

## Set the lock 
echo $$ > ${LOCKFILE}

Tentu saja, skrip ini masih memiliki cacat yang prosesnya cenderung dimulai pada saat yang sama memiliki bahaya ras, karena uji penguncian dan operasi yang ditetapkan bukan aksi atom tunggal. Tetapi solusi yang diusulkan untuk ini oleh lhunath untuk menggunakan mkdir memiliki kelemahan bahwa skrip yang terbunuh dapat meninggalkan direktori, sehingga mencegah instance lain dari berjalan.


1

The semaphoric menggunakan utilitas flock(seperti dibahas di atas, misalnya dengan presto8) untuk menerapkan penghitungan semaphore . Ini memungkinkan sejumlah proses bersamaan yang Anda inginkan. Kami menggunakannya untuk membatasi tingkat konkurensi dari berbagai proses pekerja antrian.

Ini seperti sem tapi jauh lebih ringan-berat. (Pengungkapan penuh: Saya menulisnya setelah menemukan sem terlalu berat untuk kebutuhan kita dan tidak ada utilitas semafor penghitungan yang tersedia.)


1

Contoh dengan kawanan (1) tetapi tanpa subkulit. flock () ed file / tmp / foo tidak pernah dihapus, tetapi itu tidak masalah karena mendapat flock () dan un-flock () ed.

#!/bin/bash

exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi

#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock

#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

1

Menjawab sejuta kali, tetapi dengan cara lain, tanpa perlu ketergantungan eksternal:

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

Setiap kali ia menulis PID saat ini ($$) ke dalam lockfile dan pada skrip startup memeriksa apakah suatu proses berjalan dengan PID terbaru.


1
Tanpa panggilan jebakan (atau setidaknya pembersihan dekat akhir untuk kasus normal), Anda memiliki bug positif palsu di mana file kunci ditinggalkan setelah menjalankan terakhir dan PID telah digunakan kembali oleh proses lain nanti. (Dan dalam kasus terburuk, ini diberikan proses yang panjang seperti apache ....)
Philippe Chaintreuil

1
Saya setuju, pendekatan saya cacat, perlu jebakan. Saya telah memperbarui solusi saya. Saya masih memilih untuk tidak memiliki dependensi eksternal.
Filidor Wiese

1

Menggunakan kunci proses jauh lebih kuat dan merawat pintu keluar yang tidak berterima juga. lock_file tetap terbuka selama proses ini berjalan. Ini akan ditutup (oleh shell) setelah proses ada (bahkan jika terbunuh). Saya menemukan ini sangat efisien:

lock_file=/tmp/`basename $0`.lock

if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

1

Saya menggunakan oneliner @ bagian paling awal dari skrip:

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

Adalah baik untuk melihat keberadaan proses dalam memori (tidak peduli apa status prosesnya); tetapi itu berhasil bagi saya.


0

Jalur kawanan adalah cara untuk pergi. Pikirkan tentang apa yang terjadi ketika naskah tiba-tiba mati. Dalam kasus kawanan Anda hanya kehilangan kawanan, tetapi itu tidak masalah. Juga, perhatikan bahwa trik jahat adalah mengambil kawanan pada skrip itu sendiri .. tetapi itu tentu saja memungkinkan Anda menjalankan penuh-depan ke masalah izin.


0

Cepat dan kotor?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

7
Ini mungkin atau mungkin bukan masalah, tergantung pada bagaimana penggunaannya, tetapi ada kondisi perlombaan antara pengujian untuk kunci dan membuatnya, sehingga dua skrip keduanya dapat dimulai pada saat yang sama. Jika salah satu berakhir terlebih dahulu, yang lain akan tetap berjalan tanpa file kunci.
TimB

3
C News, yang mengajari saya banyak tentang scripting shell portabel, digunakan untuk membuat file kunci. $$, dan kemudian mencoba untuk menautkannya dengan "kunci" - jika tautan berhasil, Anda memiliki kunci, jika tidak Anda menghapus kunci. $$ dan keluar.
Paul Tomblin

Itu cara yang sangat bagus untuk melakukannya, kecuali Anda masih harus menghapus lockfile secara manual jika ada kesalahan dan lockfile tidak dihapus.
Matthew Scharley

2
Cepat dan kotor, itu yang dia minta :)
Aupajo
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.