Waktu dalam skrip shell

53

Saya memiliki skrip shell yang membaca dari input standar . Dalam keadaan yang jarang terjadi, tidak akan ada orang yang siap memberikan masukan, dan skrip harus berhenti . Dalam hal batas waktu, skrip harus menjalankan beberapa kode pembersihan. Apa cara terbaik untuk melakukan itu?

Skrip ini harus sangat portabel , termasuk ke sistem unix abad ke-20 tanpa kompiler C dan ke perangkat tertanam yang menjalankan busybox, sehingga Perl, bash, bahasa yang dikompilasi, dan bahkan POSIX.2 lengkap tidak dapat diandalkan. Secara khusus, $PPID, read -tperangkap dan sempurna POSIX-compliant tidak tersedia. Menulis ke file sementara juga dikecualikan; skrip dapat berjalan bahkan jika semua sistem file dipasang hanya-baca.

Hanya untuk membuat segalanya lebih sulit, saya juga ingin skrip menjadi cukup cepat ketika tidak ada waktu habis. Secara khusus, saya juga menggunakan script di Windows (terutama di Cygwin), di mana garpu dan exec sangat rendah, jadi saya ingin menjaga penggunaannya seminimal mungkin.

Singkatnya, saya punya

trap cleanup 1 2 3 15
foo=`cat`

dan saya ingin menambahkan batas waktu. Saya tidak bisa mengganti catdengan readbuilt-in. Dalam hal timeout, saya ingin menjalankan cleanupfungsinya.


Latar Belakang: skrip ini menebak pengodean terminal dengan mencetak beberapa karakter 8-bit dan membandingkan posisi kursor sebelum dan sesudah. Awal tes skrip yang stdout terhubung ke terminal yang didukung, tetapi kadang-kadang lingkungan berbohong (misalnya plinkmengatur TERM=xtermbahkan jika itu dipanggil denganTERM=dumb ). Bagian yang relevan dari skrip terlihat seperti ini:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

Bagaimana saya bisa memodifikasi skrip sehingga jika trbelum membaca input apa pun setelah 2 detik, skrip tersebut dimatikan dan skrip mengeksekusi cleanupfungsinya?

Gilles 'SANGAT berhenti menjadi jahat'
sumber
2
Lihat juga Bagaimana cara memperkenalkan batas waktu untuk skrip shell? untuk solusi yang lebih mudah dibawa-bawa
Gilles 'SO- stop being evil'

Jawaban:

33

Bagaimana dengan ini:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

Yaitu: jalankan perintah penghasil keluaran dan sleepdalam grup proses yang sama, grup proses hanya untuk mereka. Perintah apa pun yang mengembalikan pertama membunuh seluruh proses grup.

Adakah yang bertanya-tanya: Ya, pipa itu tidak digunakan; itu dilewati menggunakan pengalihan. Tujuan satu-satunya adalah agar shell menjalankan dua proses dalam grup proses yang sama.


Seperti yang ditunjukkan Gilles dalam komentarnya, ini tidak akan berfungsi dalam shell script karena proses script akan terbunuh bersama dengan dua subproses.

Salah satu cara¹ untuk memaksa perintah untuk dijalankan dalam grup proses yang terpisah adalah memulai shell interaktif baru:

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

Tetapi mungkin ada peringatan dengan ini (misalnya ketika stdin bukan tty?). Pengarahan stderr ada di sana untuk menyingkirkan pesan "Dihentikan" ketika shell interaktif terbunuh.

Diuji dengan zsh, bashdan dash. Tapi bagaimana dengan oldies?

B98 menyarankan perubahan berikut, bekerja pada Mac OS X, dengan GNU bash 3.2.57, atau Linux dengan tanda hubung:

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`

-
1. selain setsidyang tampaknya non-standar.

Stéphane Gimenez
sumber
1
Saya sangat suka ini, saya tidak berpikir untuk menggunakan grup proses seperti itu. Ini sangat sederhana dan tidak ada kondisi balapan. Saya perlu mengujinya untuk portabilitas lagi, tetapi ini terlihat seperti pemenang.
Gilles 'SO- stop being evil'
Sayangnya, ini gagal secara spektakuler segera setelah saya menempatkan ini dalam sebuah skrip: pipa dalam substitusi perintah tidak berjalan dalam grup prosesnya sendiri, dan kill 0akhirnya membunuh penelepon skrip juga. Apakah ada cara portabel untuk memaksa pipa ke dalam kelompok prosesnya sendiri?
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles: Eek! tidak bisa menemukan cara untuk setprgp()tanpa setsiduntuk saat :-(
Stéphane Gimenez
1
Saya sangat suka triknya, jadi saya memberikan hadiahnya. Tampaknya bekerja di Linux, saya belum punya waktu untuk mengujinya di sistem lain.
Gilles 'SANGAT berhenti menjadi jahat'
2
@Zac: Tidak mungkin membalik urutan dalam kasus asli, karena hanya proses pertama yang mendapatkan akses ke stdin.
Stéphane Gimenez
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Anda sudah menjebak 15 (versi numerik SIGTERM, yaitu apa yang killdikirim kecuali dikatakan sebaliknya), jadi Anda seharusnya sudah siap. Yang mengatakan, jika Anda melihat pra-POSIX, perlu diketahui bahwa fungsi shell mungkin tidak ada (mereka berasal dari shell System V).

geekosaurus
sumber
Tidak sesederhana itu. Jika Anda menangkap sinyal, banyak shell (dash, bash, pdksh, zsh ... mungkin semua kecuali ATT ksh) mengabaikannya saat mereka sedang menunggu untuk catkeluar. Saya sudah bereksperimen sedikit tetapi tidak menemukan apa pun yang saya puas sejauh ini.
Gilles 'SO- stop being evil'
Mengenai portabilitas: untungnya saya memiliki fungsi di mana-mana. Saya tidak yakin tentang $!, saya pikir beberapa mesin yang saya gunakan jarang tidak memiliki kontrol pekerjaan, apakah $!tersedia secara universal?
Gilles 'SO- stop being evil'
@Gilles: Hm, ya. Saya ingat beberapa hal yang benar-benar mengerikan dan bashspesifik yang pernah saya lakukan melibatkan pelecehan -o monitor. Saya sedang berpikir dalam hal kerang yang benar-benar kuno ketika saya menulis itu (itu berhasil di v7). Yang mengatakan, saya pikir Anda dapat melakukan salah satu dari dua hal lain: (1) latar belakang "apa pun" dan wait $!, atau (2) juga mengirim SIGCLD/ SIGCHLD... tetapi pada mesin yang cukup lama yang terakhir adalah tidak ada atau tidak dapat diangkut (yang pertama adalah Sistem III / V, yang terakhir BSD, dan V7 tidak punya).
geekosaur
@Gilles: $!kembali ke V7 setidaknya, dan tentu saja mendahului sh-seperti yang tahu apa-apa tentang kontrol pekerjaan (pada kenyataannya, untuk waktu yang lama /bin/shdi BSD tidak melakukan kontrol pekerjaan; Anda harus berlari cshuntuk mendapatkannya - tetapi $!ada di sana ).
geekosaur
Saya tidak bisa latar belakang "apa pun", saya perlu hasilnya. Terima kasih atas info tentangnya $!.
Gilles 'SO- stop being evil'
4

Meskipun coretuils pada versi 7.0 termasuk perintah batas waktu Anda telah menyebutkan beberapa lingkungan yang tidak akan memilikinya. Untungnya pixelbeat.org memiliki skrip waktu habis yang ditulis sh.

Saya telah menggunakannya sebelumnya pada beberapa kesempatan dan ini bekerja dengan sangat baik.

http://www.pixelbeat.org/scripts/timeout ( Catatan: Skrip di bawah ini sedikit dimodifikasi dari yang ada di pixelbeat.org, lihat komentar di bawah jawaban ini.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
bahamat
sumber
Tampaknya ini tidak memungkinkan untuk mengambil output dari perintah. Lihat "Saya tidak bisa menjelaskan" apa pun "" komentar oleh Gilles di jawaban lain.
Stéphane Gimenez
Ah, menarik. Saya telah memodifikasi skrip (sehingga tidak akan cocok dengan yang ada di pixelbeat lagi) untuk mengarahkan / dev / stdin ke dalam perintah. Tampaknya berfungsi dalam pengujian saya.
bahamat
Ini tidak berfungsi untuk membuat perintah membaca dari input standar, kecuali (anehnya) di bash. </dev/stdinadalah no-op. </dev/ttyakan memungkinkannya untuk membaca dari terminal, yang cukup baik untuk kasus penggunaan saya.
Gilles 'SANGAT berhenti menjadi jahat'
@ Giles: keren, saya akan membuat pembaruan itu.
bahamat
Ini tidak dapat bekerja tanpa banyak usaha: Saya perlu mengambil output perintah, dan saya tidak bisa melakukannya jika perintah ada di latar belakang.
Gilles 'SANGAT berhenti menjadi jahat'
3

Bagaimana dengan (ab) gunakan NC untuk ini

Suka;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Atau digulung menjadi satu baris perintah;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

Versi terakhir, walaupun terlihat aneh sebenarnya tidak berfungsi ketika saya mengujinya pada sistem linux - tebakan terbaik saya adalah bahwa itu harus bekerja pada sistem apa pun, dan jika tidak variasi redirection output dapat memecahkan masalah portabilitas. manfaat di sini adalah tidak ada proses latar belakang yang terlibat.

Soren
sumber
Tidak cukup portabel. Saya tidak memiliki ncbeberapa kotak lama Unix, atau pada banyak Linux yang tertanam.
Gilles 'SO- stop being evil'
0

Cara lain untuk menjalankan pipeline di grup prosesnya sendiri adalah dengan menjalankan sh -c '....'pseudo-terminal menggunakan scriptperintah (yang secara implisit menerapkan setsidfungsi).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"
pete
sumber
Tidak cukup portabel. Saya tidak memiliki scriptbeberapa kotak lama Unix, atau pada banyak Linux yang tertanam.
Gilles 'SO- stop being evil'
0

Jawabannya di https://unix.stackexchange.com/a/18711 sangat bagus.

Saya ingin mencapai hasil yang serupa, tetapi tanpa harus secara eksplisit memanggil shell lagi karena saya ingin memanggil fungsi shell yang ada.

Menggunakan bash, dimungkinkan untuk melakukan hal berikut:

eval 'set -m ; ( ... ) ; '"$(set +o)"

Jadi misalkan saya sudah memiliki fungsi shell f:

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

Menjalankan ini saya melihat:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
Pangeran
sumber
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
sumber
Ini tidak berfungsi karena alasan yang sama dengan jawaban geekausor . Saya perlu mendapatkan output dari perintah eksternal, dan sinyal diabaikan saat perintah ini sedang berjalan.
Gilles 'SANGAT berhenti menjadi jahat'