Co się stanie, jeśli edytujesz skrypt podczas wykonywania?

31

Mam ogólne pytanie, które może wynikać z niezrozumienia sposobu obsługi procesów w systemie Linux.

Dla moich celów zamierzam zdefiniować „skrypt” jako fragment kodu bash zapisanego w pliku tekstowym z włączonymi uprawnieniami do wykonywania dla bieżącego użytkownika.

Mam serię skryptów, które wzywają się w tandemie. Dla uproszczenia nazywam je skryptami A, B i C. Skrypt A wykonuje serię instrukcji, a następnie zatrzymuje się, następnie wykonuje skrypt B, następnie zatrzymuje się, a następnie wykonuje skrypt C. Innymi słowy, seria kroków jest mniej więcej tak:

Uruchom skrypt A:

  1. Seria instrukcji
  2. Pauza
  3. Uruchom skrypt B
  4. Pauza
  5. Uruchom skrypt C

Wiem z doświadczenia, że ​​jeśli uruchomię skrypt A do pierwszej pauzy, a następnie dokonam zmian w skrypcie B, zmiany te zostaną odzwierciedlone w wykonaniu kodu, gdy pozwolę mu wznowić. Podobnie, jeśli dokonam zmian w skrypcie C, gdy skrypt A jest nadal wstrzymany, pozwól mu kontynuować po zapisaniu zmian, zmiany te zostaną odzwierciedlone w wykonaniu kodu.

Oto prawdziwe pytanie: czy jest jakiś sposób edycji Skryptu A, gdy jest on jeszcze uruchomiony? A może edycja jest niemożliwa po rozpoczęciu jej wykonywania?

Kofeina Koneser
źródło
2
pomyślałbym, że to zależy od powłoki. chociaż twierdzisz, że używasz bash. wygląda na to, że byłoby to zależne od sposobu, w jaki powłoka ładuje skrypty wewnętrznie.
strugee
zachowanie może się również zmienić, jeśli plik zostanie pobrany zamiast źródła.
strugee
1
Myślę, że bash wczytuje cały skrypt do pamięci przed wykonaniem.
w4etwetewtwet
2
@handuel, nie, nie ma. Jakby nie czekał, aż wpiszesz „exit” w monicie, aby rozpocząć interpretację wprowadzonych poleceń.
Stéphane Chazelas,
1
@StephaneChazelas Tak, czytając z terminala tak nie jest, ale różni się to od uruchamiania skryptu.
w4etwetewtwet

Odpowiedzi:

21

W Uniksie większość edytorów pracuje, tworząc nowy plik tymczasowy zawierający edytowaną zawartość. Po zapisaniu edytowanego pliku oryginalny plik jest usuwany, a plik tymczasowy zostaje przemianowany na oryginalną nazwę. (Istnieją oczywiście różne zabezpieczenia zapobiegające utracie danych.) Jest to na przykład styl używany przez flagę („na miejscu”) sedlub perlwywoływany z nią -i, co wcale nie jest tak naprawdę „na miejscu”. Powinno to zostać nazwane „nowym miejscem o starej nazwie”.

Działa to dobrze, ponieważ unix zapewnia (przynajmniej dla lokalnych systemów plików), że otwarty plik istnieje, dopóki nie zostanie zamknięty, nawet jeśli zostanie „usunięty” i utworzony zostanie nowy plik o tej samej nazwie. (To nie przypadek, że systemowe wywołanie unixa w celu „usunięcia” pliku nazywa się w rzeczywistości „unlink”.) Ogólnie rzecz biorąc, jeśli interpreter powłoki ma otwarty plik źródłowy, a plik „edytujesz” w sposób opisany powyżej , powłoka nawet nie zobaczy zmian, ponieważ nadal ma otwarty oryginalny plik.

[Uwaga: podobnie jak w przypadku wszystkich komentarzy opartych na standardach, powyższe podlega wielu interpretacjom i istnieją różne przypadki narożne, takie jak NFS. Zachęcamy do wypełniania komentarzy wyjątkami.]

Oczywiście można bezpośrednio modyfikować pliki; jest to po prostu mało wygodne do edycji, ponieważ chociaż możesz zastąpić dane w pliku, nie możesz go usunąć ani wstawić bez przesunięcia wszystkich następujących danych, co wymagałoby sporo przepisywania. Co więcej, podczas wykonywania tego przesunięcia zawartość pliku byłaby nieprzewidywalna i ucierpiałyby procesy, w których plik byłby otwarty. Aby tego uniknąć (na przykład w przypadku systemów baz danych), potrzebujesz zaawansowanego zestawu protokołów modyfikacji i rozproszonych blokad; rzeczy, które znacznie wykraczają poza zakres typowego narzędzia do edycji plików.

Jeśli więc chcesz edytować plik przetwarzany przez powłokę, masz dwie opcje:

  1. Możesz dołączyć do pliku. To zawsze powinno działać.

  2. Możesz zastąpić plik nową zawartością o dokładnie takiej samej długości . Może to działać lub nie, w zależności od tego, czy powłoka już odczytała tę część pliku, czy nie. Ponieważ większość operacji we / wy na plikach obejmuje bufory odczytu, a ponieważ wszystkie znane mi powłoki odczytują całą komendę złożoną przed jej wykonaniem, jest mało prawdopodobne, abyś mógł sobie z tym poradzić. Z pewnością nie byłby niezawodny.

Nie znam żadnego sformułowania w standardzie Posix, które faktycznie wymaga możliwości dołączenia do pliku skryptu podczas wykonywania pliku, więc może nie działać z każdą powłoką zgodną z Posix, a tym bardziej z obecną ofertą prawie- i czasami powłoki zgodne z posix. Więc YMMV. Ale o ile wiem, działa niezawodnie z bash.

Jako dowód, oto „pozbawiona pętli” implementacja niesławnego programu 99 butelek piwa w bash, który wykorzystuje dddo nadpisywania i dołączania (nadpisywanie jest prawdopodobnie bezpieczne, ponieważ zastępuje aktualnie wykonywaną linię, która jest zawsze ostatnią linią plik z komentarzem o dokładnie tej samej długości; zrobiłem to, aby wynik końcowy mógł zostać wykonany bez zachowania samodmodyfikującego).

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
rici
źródło
Kiedy uruchomię to, zaczyna się od „No more”, a następnie przechodzi do -1 i do liczb ujemnych w nieskończoność.
Daniel Hershcovich,
Jeśli zrobię to export beer=100przed uruchomieniem skryptu, działa on zgodnie z oczekiwaniami.
Daniel Hershcovich
@DanielHershcovich: całkiem słusznie; niechlujne testowanie z mojej strony. Myślę, że to naprawiłem; teraz przyjmuje opcjonalny parametr zliczania. Lepszym i ciekawszym rozwiązaniem byłoby automatyczne zresetowanie, jeśli parametr nie odpowiada kopii w pamięci podręcznej.
rici
18

bash przechodzi długą drogę, aby upewnić się, że czyta polecenia tuż przed ich wykonaniem.

Na przykład w:

cmd1
cmd2

Powłoka będzie czytać skrypt według bloków, więc prawdopodobnie odczyta oba polecenia, zinterpretuje pierwsze, a następnie przejdzie do końca cmd1skryptu i ponownie przeczyta skrypt, aby go przeczytać cmd2i wykonać.

Możesz to łatwo zweryfikować:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(choć patrząc na stracewyniki, wydaje się, że robi to trochę bardziej wymyślne rzeczy (np. przeczytaj dane kilka razy, szukaj wstecz ...) niż kiedy próbowałem tego samego kilka lat temu, więc moje powyższe stwierdzenie o szukaniu wstecz może nie dotyczy już nowszych wersji).

Jeśli jednak napiszesz skrypt jako:

{
  cmd1
  cmd2
  exit
}

Powłoka będzie musiała odczytać do zamknięcia }, zapisać ją w pamięci i wykonać. Z tego powodu exitpowłoka nie będzie ponownie czytać ze skryptu, dzięki czemu można go bezpiecznie edytować podczas interpretacji powłoki.

Alternatywnie podczas edytowania skryptu pamiętaj, aby napisać nową kopię skryptu. Powłoka będzie nadal czytać oryginalną (nawet jeśli zostanie usunięta lub zostanie zmieniona jej nazwa).

Aby to zrobić, zmienić the-script, aby the-script.oldskopiować the-script.olddo the-scripti edytować.

Stéphane Chazelas
źródło
4

Naprawdę nie ma bezpiecznego sposobu na modyfikację skryptu podczas jego działania, ponieważ powłoka może użyć buforowania do odczytania pliku. Ponadto, jeśli skrypt zostanie zmodyfikowany przez zastąpienie go nowym plikiem, powłoki zwykle będą czytać nowy plik tylko po wykonaniu pewnych operacji.

Często, gdy skrypt jest zmieniany podczas wykonywania, powłoka kończy zgłaszanie błędów składniowych. Wynika to z faktu, że gdy powłoka zamyka i ponownie otwiera plik skryptu, używa przesunięcia bajtu do pliku, aby zmienić położenie po powrocie.

popiół
źródło
4

Możesz obejść ten problem, ustawiając pułapkę na skrypcie, a następnie używając, execaby pobrać nową zawartość skryptu. Zauważ jednak, że execwywołanie rozpoczyna skrypt od zera, a nie od miejsca, w którym dotarł w trakcie procesu, więc skrypt B zostanie wywołany (itd.).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

To będzie nadal wyświetlać datę na ekranie. Mógłbym wtedy edytować skrypt i zmienić datena echo "Date: $(date)". Po napisaniu tego uruchomiony skrypt nadal wyświetla tylko datę. Jakkolwiek kiedykolwiek wyślę sygnał, który ustawiłem na trapprzechwytywanie, skrypt będzie exec(zastępuje bieżący uruchomiony proces określonym poleceniem), którym jest polecenie $CMDi argumenty $@. Możesz to zrobić, wydając kill -1 PID- gdzie PID jest PID uruchomionego skryptu - a dane wyjściowe zmieniają się, aby pokazać się Date:przed datewyjściem komendy.

Możesz zapisać „stan” skryptu w zewnętrznym pliku (w say / tmp) i przeczytać zawartość, aby dowiedzieć się, gdzie „wznowić”, kiedy program zostanie ponownie uruchomiony. Możesz następnie dodać dodatkowe zakończenie pułapek (SIGINT / SIGQUIT / SIGKILL / SIGTERM), aby wyczyścić ten plik tmp, więc po ponownym uruchomieniu po przerwaniu „Skryptu A” rozpocznie się od początku. Wersja stanowa wyglądałaby mniej więcej tak:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Drav Sloan
źródło
Rozwiązałem ten problem, przechwytując $0i $@na początku skryptu i używając tych zmiennych w execzamian.
Drav Sloan