Jak czekać na plik w skrypcie powłoki?

15

Próbuję napisać skrypt powłoki, który będzie oczekiwał na pojawienie się pliku w /tmpkatalogu o nazwie, sleep.txta gdy go znajdzie, program przestanie działać, w przeciwnym razie chcę, aby program był w stanie uśpienia (zawieszenia) do momentu zlokalizowania pliku . Teraz zakładam, że użyję polecenia testowego. Coś w stylu

(if [ -f "/tmp/sleep.txt" ]; 
then stop 
   else sleep.)

Jestem zupełnie nowy w pisaniu skryptów powłoki i każda pomoc jest mile widziana!

Mo Gainz
źródło
Patrzeć $MAILPATH.
mikeserv

Odpowiedzi:

23

Pod Linuksem możesz użyć podsystemu jądra inotify, aby efektywnie czekać na pojawienie się pliku w katalogu:

while read i; do if [ "$i" = sleep.txt ]; then break; fi; done \
   < <(inotifywait  -e create,open --format '%f' --quiet /tmp --monitor)
# script execution continues ...

(przy założeniu Bash dla <()składni przekierowania wyjściowego)

Zaletą tego podejścia w porównaniu do odpytywania w ustalonych odstępach czasu, jak w

while [ ! -f /tmp/sleep.txt ]; do sleep 1; done
# script execution continues ...

jest to, że jądro śpi więcej. Dzięki specyfikacji zdarzenia inotify, takiej jak create,openskrypt, jest właśnie zaplanowany do wykonania po /tmputworzeniu lub otwarciu pliku poniżej . Dzięki ustalonemu odpytywaniu przedziału czasu marnujesz cykle procesora dla każdego przyrostu czasu.

Zawarłem openzdarzenie, aby zarejestrować się, touch /tmp/sleep.txtgdy plik już istnieje.

maxschlepzig
źródło
Dzięki, to jest niesamowite! Dlaczego potrzebujesz pętli while, jeśli znasz nazwę pliku? Dlaczego tak nie było inotifywait -e create,open --format '%f' --quiet /tmp/sleep.txt --monitor > /dev/null ; # continues once /tmp/sleep.txt is created/opened?
NHDaly,
Och, jk. Po zainstalowaniu narzędzia rozumiem teraz. inotifywaitjest pętlą while i wyświetla linię za każdym razem, gdy plik jest otwierany / tworzony. Więc potrzebujesz pętli while, aby przerwać, gdy zostanie zmieniona.
NHDaly,
z wyjątkiem tego, że powoduje to wyścig - w przeciwieństwie do wersji testowej -e / sleep. Nie ma sposobu, aby zagwarantować, że plik nie zostanie utworzony tuż przed wejściem w pętlę - w takim przypadku zdarzenie zostanie pominięte. Musisz dodać coś takiego (sleep 1; [[-e /tmp/sleep.txt]] && touch /tmp/sleep.txt;) & przed pętlą. Co oczywiście może powodować problemy w przyszłości, w zależności od rzeczywistej logiki biznesowej
n-Alexander
@ n-aleksander Cóż, odpowiedź zakłada, że ​​wykonujesz fragment kodu przed rozpoczęciem procesu, który wygeneruje sleep.txtw pewnym momencie. Zatem nie ma warunków rasowych. Pytanie nie zawiera niczego, co sugerowałoby scenariusz, który masz na myśli.
maxschlepzig
@maxschlepzig przeciwnie. Bez zamówienia nie można tego założyć. Podstawy
n-aleksander
10

Po prostu umieść test w whilepętli:

while [ ! -f /tmp/sleep.txt ]; do sleep 1; done
# next command
jimmij
źródło
Tak jak napisałeś, jest to logika: dopóki plik nie istnieje, skrypt będzie spał. W przeciwnym razie jest to zrobione. Poprawny?
Mo Gainz
@MoGainz Correct. Liczba po sleepto liczba sekund oczekiwania w każdej iteracji - możesz to zmienić w zależności od potrzeb.
jimmij
7

Istnieje kilka problemów z niektórymi dotychczasowymi inotifywaitpodejściami opartymi na:

  • nie mogą znaleźć sleep.txtpliku, który został najpierw utworzony jako nazwa tymczasowa, a następnie zmieniono jego nazwę na sleep.txt. moved_toOprócz tego trzeba dopasować do wydarzeńcreate
  • nazwy plików mogą zawierać znaki nowej linii, wypisanie nazw plików rozdzielanych nową linią nie wystarczy, aby ustalić, czy plik sleep.txtzostał utworzony. Co jeśli na przykład foo\nsleep.txt\nbarplik został utworzony?
  • co jeśli plik zostanie utworzony przed inotifywait uruchomieniem i zainstalowaniem zegarka? Potem inotifywaitczekałby wiecznie na plik, który już tu jest. Musisz upewnić się, że pliku już nie ma po zainstalowaniu zegarka.
  • niektóre rozwiązania przestają inotifywaitdziałać (przynajmniej do momentu utworzenia innego pliku) po znalezieniu pliku.

Aby rozwiązać te problemy, możesz:

sh -c 'echo "$$" &&
        LC_ALL=C exec inotifywait -me create,moved_to --format=/%f/ . 2>&1' | {
  IFS= read pid &&
    while IFS= read -r line && [ "$line" != "Watches established." ]; do
      : wait for watches to be established
    done
  [ -e sleep.txt ] || [ -L sleep.txt ] || grep -qxF /sleep.txt/ && kill "$pid"
}

Zauważ, że obserwujemy tworzenie sleep.txtw bieżącym katalogu .(więc zrobiłbyś to cd /tmp || exitwcześniej w swoim przykładzie). Bieżący katalog nigdy się nie zmienia, więc gdy ten rurociąg potoczy się pomyślnie, znajduje się sleep.txtw utworzonym katalogu bieżącym.

Oczywiście, można zastąpić .z /tmppowyżej, ale gdy inotifywaitjest uruchomiony, /tmpmogła zostać zmieniona na kilka razy (mało prawdopodobne /tmp, ale coś do rozważenia w przypadku ogólnym) lub nowy system plików zamontowany na nim, więc kiedy wraca do rurociągów, nie mogą być /tmp/sleep.txtstworzonym, ale /new-name-for-the-original-tmp/sleep.txtzamiast niego. W /tmptym przedziale czasu można także utworzyć nowy katalog, którego nie można oglądać, więc sleep.txtutworzony tam katalog nie zostanie wykryty.

Stéphane Chazelas
źródło
1

Akceptowana odpowiedź naprawdę działa (dzięki maxschlepzig), ale pozostawia monitorowanie inotifywait w tle, dopóki skrypt nie zakończy działania. Jedyną odpowiedzią, która dokładnie odpowiada twoim wymaganiom (tj. Oczekiwanie na pojawienie się pliku sleep.txt w / tmp) wydaje się być odpowiedź Stephane'a, jeśli katalog, który ma być monitorowany przez inotifywait, zostanie zmieniony z kropki (.) Na '/ tmp'.

Jeśli jednak chcesz użyć katalogu tymczasowego TYLKO do umieszczenia flagi sleep.txt i możesz się założyć, że nikt inny nie umieści żadnego pliku w tym katalogu, wystarczy poprosić inotifywait o obejrzenie tego katalogu pod kątem tworzenia plików:

Pierwszy krok: utwórz katalog, który będziesz monitorować:

directoryToPutSleepFile=$(mktemp -d)

Drugi krok: upewnij się, że katalog naprawdę tam jest

until [ -d $directoryToPutSleepFile ]; do sleep 0.1; done

Trzeci krok: poczekaj, aż pojawi się ŻADNY plik $directoryToPutSleepFile

inotifywait -e create --format '%f' --quiet $directoryToPutSleepFile

Plik, który umieścisz, $directoryToPutSleepFilemoże mieć nazwę sleep.txt awake.txt, cokolwiek. Moment utworzenia dowolnego pliku w $directoryToPutSleepFileskrypcie będzie przebiegał poza inotifywaitinstrukcją.

Nikolaos
źródło
1
Może to jednak pomijać tworzenie pliku, jeśli inny został utworzony tuż przedtem, powodując sleep.txtutworzenie między dwoma wywołaniami inotifywait.
Stéphane Chazelas
zobacz moją odpowiedź na alternatywny sposób rozwiązania tego problemu.
Stéphane Chazelas
Proszę wypróbować mój kod. inotifywait jest wywoływany tylko raz. Kiedy tworzony jest sleep.txt (lub czytany / zapisywany, jeśli już tam jest), inotifywait wyświetla „sleep.txt”, a wykonywanie jest kontynuowane po instrukcji „done”.
Nikolaos
Jest wywoływany kilka razy, dopóki nie sleep.txtzostanie utworzony plik o nazwie . Spróbuj na przykład touch /tmp/foo /tmp/sleep.txt, wtedy jedno wystąpienie inotifywaitzgłosi fooi zakończy działanie, a do czasu uruchomienia następnego inotifywait sleep.txt już długo tam będzie, więc inotifywait nie wykryje jego utworzenia.
Stéphane Chazelas
Miałeś rację co do wielu wywołań: jedno wywołanie dla każdego pliku utworzonego w / tmp. Zaktualizowałem swoją odpowiedź. Dzięki za komentarz.
Nikolaos
0

ogólnie rzecz biorąc, bardzo trudno jest uczynić cokolwiek innego niż prostą pętlę test / sen. Głównym problemem jest to, że test będzie się ścigał z tworzeniem pliku - chyba że test się powtórzy, zdarzenie tworzenia może zostać pominięte. Jest to zdecydowanie najlepszy wybór w większości przypadków, w których powinieneś używać Shell na początek:

#!/bin/bash
while [[ ! -e /tmp/file ]] ; do
    sleep 1
done

Jeśli okaże się, że jest to niewystarczające ze względu na problemy z wydajnością, to użycie Shell prawdopodobnie nie jest dobrym pomysłem.

n-Alexander
źródło
2
To tylko kopia odpowiedzi Jimmij .
maxschlepzig
0

W oparciu o zaakceptowaną odpowiedź maxschlepzig (i pomysł z zaakceptowanej odpowiedzi na /superuser/270529/monitoring-a-file-until-a-string-is-found ) proponuję następujące ulepszenia ( moim zdaniem) odpowiedź, która może również działać z limitem czasu:

# Enable pipefail, so if the left side of the pipe fails it does not get silently ignored
set -o pipefail
( timeout 120 inotifywait -e create,open --format '%f' --quiet /tmp --monitor & ) | while read i; do if [ "$i" == 'sleep.txt' ]; then break; fi; done
EXIT_STATUS=$?
if [ "${EXIT_STATUS}" == '124' ]; then
  echo "Timeout happened"
fi

W przypadku, gdy plik nie zostanie utworzony / otwarty w podanym limicie czasu, status wyjścia to 124 (zgodnie z dokumentacją limitu czasu (strona podręcznika)). W przypadku, gdy zostanie utworzony / otwarty, kodem wyjścia jest 0 (sukces).

Tak, inotifywait działa w ten sposób w podpowłoce i ta podpowłoka zakończy działanie dopiero po przekroczeniu limitu czasu lub po zakończeniu skryptu głównego (w zależności od tego, co nastąpi wcześniej).

Attila123
źródło